设为首页 收藏本站
查看: 1440|回复: 0

[经验分享] Python基础:新式类的属性访问

[复制链接]

尚未签到

发表于 2015-4-20 11:12:02 | 显示全部楼层 |阅读模式

  • 一、概述
  • 二、准备工作

    • 1、讨论对象
    • 2、名词解释


  • 三、实例绑定的属性访问

    • 1、获取属性

      • 一般规则
      • 参考源码
      • 示例验证


    • 2、设置属性

      • 一般规则
      • 参考源码
      • 示例验证


    • 3、删除属性

      • 一般规则
      • 参考源码
      • 示例验证




  • 四、类绑定的属性访问

    • 1、获取属性

      • 一般规则
      • 参考源码
      • 示例验证


    • 2、设置属性

      • 一般规则
      • 参考源码
      • 示例验证


    • 3、删除属性

      • 一般规则
      • 参考源码
      • 示例验证




  • 五、更多细节

    • 1、属性的设置与删除
    • 2、描述符

      • 区分处理
      • 使用惯例




  • 六、简单自测

一、概述
  自从Python 2.2引入新式类(New-style classes)以后,元类(Metaclass)、描述符(Descriptor)和一些特殊方法(如__getattribute__)的出现,使得原本简单的 属性访问(Attribute access)变得复杂起来。
  对于 新式类的属性访问 这一主题,官方文档 Customizing attribute access 和 Descriptor HowTo Guide 都是很好的参考,但感觉讲得还不够全面、通透。本文结合 官方文档Python 2.7源码,尝试给出 属性访问的一般规则
  在以下讨论中,根据触发方式的不同,属性访问分为 实例绑定的属性访问类绑定的属性访问;而根据操作类型的不同,访问又包括 获取设置删除

二、准备工作

1、讨论对象
  下面的讨论会涉及五个对象:


  • 实例a
  • 类A
  • 元类MetaA
  • 描述符类Descr
  • 属性attr
  它们之间的关系如下:


  • a是A的实例
  • A是MetaA的实例
  • attr可能是普通属性,也可能是描述符(此时,attr是Descr的实例)
  • attr可能位于a的实例字典中,也可能位于A的MRO的类字典中,还可能位于MetaA的MRO的类字典中

2、名词解释
  以下是讨论过程中会用到的名词:


  • 实例绑定:通过实例访问属性的方式,如a.attr
  • 类绑定:通过类访问属性的方式,如A.attr
  • 实例字典:实例中的属性字典,如a.__dict__
  • 类字典:类中的属性字典,如A.__dict__
  • 类的MRO(Method Resolution Order):类及其基类组成的序列,如A.__mro__
  • 元类:用于创建类的类,如MetaA
  • 普通属性:不是描述符的属性
  • 描述符:如果一个类(如Descr)中存在__get__、__set__和__delete__三种特殊方法的任意组合,那么该类的实例就是一个描述符
  • 数据描述符(data descriptor):定义了__get__和__set__的描述符
  • 非数据描述符(non-data descriptor):只定义了__get__的描述符

三、实例绑定的属性访问

1、获取属性

一般规则
  a.attr对应的访问规则为:


  •   首先查找A中是否覆盖了特殊方法__getattribute__:

    • 存在则使用覆盖版本,直接返回A.__getattribute__(a, 'attr')
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找A.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr:

      • 如果attr是数据描述符,则为情况(case_a)
      • 如果attr是非数据描述符,则为情况(case_b)
      • 如果attr是普通属性,则为情况(case_c)


    • 如果没有找到attr,则为情况(case_d)


  •   如果为情况(case_a),则返回Descr.__get__(attr, a, A)

  • 否则查找实例字典a.__dict__中是否存在属性attr,存在则返回attr
  • 否则如果为情况(case_b),则返回Descr.__get__(attr, a, A)
  • 否则如果为情况(case_c),则返回attr
  • 否则如果为情况(case_d)或者上述步骤抛出了AttributeError异常,则查找A中是否存在特殊方法__getattr__,存在则返回A.__getattr__(a, 'attr')
  • 否则不存在属性attr,抛出AttributeError异常

参考源码
  PyObject_GenericGetAttr

示例验证

# 步骤8:不存在属性attr,抛出AttributeError异常
>>> class A(object): pass
...
>>> a = A()
>>> a.attr
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'A' object has no attribute 'attr'
# 步骤7:A中存在特殊方法__getattr__,返回A.__getattr__(a, 'attr')
>>> class A(object):
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'attr in __getattr__'
# 步骤6:类字典A.__dict__中存在普通属性attr,返回A.__dict__['attr']
>>> class A(object):
...     attr = 'ordinary attribute in A'
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'ordinary attribute in A'
# 步骤5:类字典A.__dict__中存在非数据描述符attr,返回Descr.__get__(attr, a, A)
>>> class Descr(object):
...     def __get__(self, instance, owner):
...         return 'non-data descriptor in A'
...
>>> class A(object):
...     attr = Descr()
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'non-data descriptor in A'
# 步骤4:实例字典a.__dict__中存在属性attr,返回a.__dict__['attr']
>>> class Descr(object):
...     def __get__(self, instance, owner):
...         return 'non-data descriptor in A'
...
>>> class A(object):
...     attr = Descr()
...     def __init__(self):
...         self.attr = 'attribute in a'
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'attribute in a'
# 步骤3:类字典A.__dict__中存在数据描述符attr,返回Descr.__get__(attr, a, A)
>>> class Descr(object):
...     def __get__(self, instance, owner):
...         return 'data descriptor in A'
...     def __set__(self, instance, value):
...         pass
...
>>> class A(object):
...     attr = Descr()
...     def __init__(self):
...         self.attr = 'attribute in a'
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'data descriptor in A'
# 步骤1:A中覆盖了特殊方法__getattribute__,返回A.__getattribute__(a, 'attr')
>>> class Descr(object):
...     def __get__(self, instance, owner):
...         return 'data descriptor in A'
...     def __set__(self, instance, value):
...         pass
...
>>> class A(object):
...     attr = Descr()
...     def __init__(self):
...         self.attr = 'attribute in a'
...     def __getattribute__(self, name):
...         return name + ' in __getattribute__'
...     def __getattr__(self, name):
...         return name + ' in __getattr__'
...
>>> a = A()
>>> a.attr
'attr in __getattribute__'

2、设置属性

一般规则
  a.attr = value对应的访问规则为:


  •   首先查找A中是否覆盖了特殊方法__setattr__:

    • 存在则使用覆盖版本,直接调用A.__setattr__(a, 'attr', value)
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找A.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr,如果attr是描述符(定义__set__即可,参考 『更多细节』),则调用Descr.__set__(attr, a, value)
    • 否则(attr是未定义__set__的描述符或普通属性,或者没有找到attr),跳到步骤3


  •   在实例字典a.__dict__中设置(有则改之,无则加之)属性attr


参考源码
  PyObject_GenericSetAttr

示例验证

# 步骤3:在实例字典a.__dict__中设置属性attr,即执行a.__dict__['attr'] = value
>>> class A(object): pass
...
>>> a = A()
>>> a.attr = 'newbie'
>>> a.__dict__['attr']
'newbie'
# 步骤2:类字典A.__dict__中存在定义了__set__的描述符,调用Descr.__set__(attr, a, value)
>>> class Descr(object):
...     def __set__(self, instance, value):
...         print('set {0!r} within descriptor'.format(value))
...
>>> class A(object):
...     attr = Descr()
...
>>> a = A()
>>> a.attr = 'newbie'
set 'newbie' within descriptor
# 步骤1:A中覆盖了特殊方法__setattr__,调用A.__setattr__(a, 'attr', value)
>>> class Descr(object):
...     def __set__(self, instance, value):
...         print('set {0!r} within descriptor'.format(value))
...
>>> class A(object):
...     attr = Descr()
...     def __setattr__(self, name, value):
...         print('set {0!r} in __setattr__'.format(value))
...
>>> a = A()
>>> a.attr = 'newbie'
set 'newbie' in __setattr__

3、删除属性

一般规则
  del a.attr对应的访问规则为:


  •   首先查找A中是否覆盖了特殊方法__delattr__:

    • 存在则使用覆盖版本,直接调用A.__delattr__(a, 'attr')
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找A.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr,如果attr是描述符(定义__delete__即可,参考 『更多细节』),则调用Descr.__delete__(attr, a)
    • 否则(attr是未定义__delete__的描述符或普通属性,或者没有找到attr),跳到步骤3


  •   如果实例字典a.__dict__中存在属性attr,则删除该属性

  • 否则无法删除不存在的属性attr,抛出AttributeError异常

参考源码
  PyObject_GenericSetAttr(参考 『更多细节』)

示例验证

# 步骤4:无法删除不存在的属性attr,抛出AttributeError异常
>>> class A(object): pass
...
>>> a = A()
>>> del a.attr
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'A' object has no attribute 'attr'
# 步骤3:实例字典a.__dict__中存在属性attr,删除该属性
>>> class A(object):
...     def __init__(self):
...         self.attr = 'dying'
...
>>> a = A()
>>> del a.attr
# 步骤2:类字典A.__dict__中存在定义了__delete__的描述符,调用Descr.__delete__(attr, a)
>>> class Descr(object):
...     def __delete__(self, instance):
...         print('delete within descriptor')
...
>>> class A(object):
...     attr = Descr()
...
>>> a = A()
>>> del a.attr
delete within descriptor
# 步骤1:A中覆盖了特殊方法__delattr__,调用A.__delattr__(a, 'attr')
>>> class Descr(object):
...     def __delete__(self, instance):
...         print('delete within descriptor')
...
>>> class A(object):
...     attr = Descr()
...     def __delattr__(self, name):
...         print('delete in __delattr__')
...
>>> a = A()
>>> del a.attr
delete in __delattr__

四、类绑定的属性访问
  在上述对 实例绑定的属性访问 的讨论中,如果把 实例a 换成 类A,把 类A 换成 元类MetaA,几乎就是 类绑定的属性访问 的全过程。
  是的,两种访问过程的算法模型几乎完全一致,只有非常微小的差异。从这一点上,也可以看出Python语言的设计是非常优秀的:Special cases aren't special enough to break the rules。“拥抱一致性,减少特例”,这也是值得我们学习的态度。
  在以下讨论中,为了保证结论的完整性,会给出 一般规则 的全貌,并特别指出差异点;但为了DRY(Don’t Repeat Yourself),将不再给出 示例验证 部分,因为只要明白 “元类MetaA 与 类A” 和 “类A 与 实例a” 是关系对等的,就可以举一反三了(如果不明白,可以参考 Python基础:元类)。

1、获取属性

一般规则
  A.attr对应的访问规则为:


  •   首先查找MetaA中是否覆盖了特殊方法__getattribute__:

    • 存在则使用覆盖版本,直接返回MetaA.__getattribute__(A, 'attr')
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找MetaA.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr:

      • 如果attr是数据描述符,则为情况(case_a)
      • 如果attr是非数据描述符,则为情况(case_b)
      • 如果attr是普通属性,则为情况(case_c)


    • 如果没有找到attr,则为情况(case_d)


  •   如果为情况(case_a),则返回Descr.__get__(attr, A, MetaA)

  •   否则依次查找A.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr:

      • 如果attr是描述符(定义__get__即可),则返回Descr.__get__(attr, None, A)
      • 如果attr是未定义__get__的描述符或普通属性,则直接返回attr


    • 如果没有找到attr,跳到步骤5


  •   否则如果为情况(case_b),则返回Descr.__get__(attr, A, MetaA)

  • 否则如果为情况(case_c),则返回attr
  • 否则如果为情况(case_d)或者上述步骤抛出了AttributeError异常,则查找MetaA中是否存在特殊方法__getattr__,存在则返回MetaA.__getattr__(A, 'attr')
  • 否则不存在属性attr,抛出AttributeError异常
  注意:差异点在 步骤4

参考源码
  type_getattro

示例验证
  请举一反三

2、设置属性

一般规则
  A.attr = value对应的访问规则为:


  •   首先查找MetaA中是否覆盖了特殊方法__setattr__:

    • 存在则使用覆盖版本,直接调用MetaA.__setattr__(A, 'attr', value)
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找MetaA.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr,如果attr是描述符(定义__set__即可,参考 『更多细节』),则调用Descr.__set__(attr, A, value)
    • 否则(attr是未定义__set__的描述符或普通属性,或者没有找到attr),跳到步骤3


  •   在类字典A.__dict__中设置(有则改之,无则加之)属性attr


参考源码
  type_setattro

示例验证
  请举一反三

3、删除属性

一般规则
  del A.attr对应的访问规则为:


  •   首先查找MetaA中是否覆盖了特殊方法__delattr__:

    • 存在则使用覆盖版本,直接调用MetaA.__delattr__(A, 'attr')
    • 没有覆盖则使用默认版本,跳到步骤2


  •   依次查找MetaA.__mro__的类字典__dict__中是否存在属性attr:

    • 对于第一个找到的attr,如果attr是描述符(定义__delete__即可,参考 『更多细节』),则调用Descr.__delete__(attr, A)
    • 否则(attr是未定义__delete__的描述符或普通属性,或者没有找到attr),跳到步骤3


  •   如果类字典A.__dict__中存在属性attr,则删除该属性

  • 否则无法删除不存在的属性attr,抛出AttributeError异常

参考源码
  type_setattro(参考 『更多细节』)

示例验证
  请举一反三

五、更多细节

1、属性的设置与删除
  CPython实现中,删除属性 被视为是 设置属性 的一种特殊情况(参考 PyObject_DelAttr):

#define  PyObject_DelAttr(O,A) PyObject_SetAttr((O),(A),NULL)

  因此,在上述讨论的 参考源码 中,您会发现 设置属性删除属性 调用的函数其实是一样的。

2、描述符

区分处理
  以 实例绑定的属性访问 为例(类绑定的属性访问 类似),如果 设置属性删除属性 最终都调用PyObject_GenericSetAttr,那么在判断描述符的时候,又是如何区分并调用__set__和__delete__的呢?
  实际上,PyObject_GenericSetAttr最终调用了_PyObject_GenericSetAttrWithDict,观察函数_PyObject_GenericSetAttrWithDict中 对描述符的判断方法,我们可以发现:只要函数指针tp_descr_set不为空,就会调用它指向的函数完成操作。
  而在 数组slotdefs 中,我们又发现__set__和__delete__都对应同样的函数指针tp_descr_set,并被赋值指向同一个函数slot_tp_descr_set;更进一步地,在函数slot_tp_descr_set中,会判断入参指针value,如果为空则调用__delete__,否则调用__set__。此时,再回头看看PyObject_DelAttr和PyObject_SetAttr的区别,我们会发现 删除设置 的区分标准是一致的。
  至此,问题的答案应该很清楚了:


  • 如果定义了__set__,函数指针tp_descr_set就不为空,就会进一步调用函数slot_tp_descr_set,并在该函数中再实际调用函数__set__
  • 如果定义了__delete__,函数指针tp_descr_set也不为空,也会进一步调用函数slot_tp_descr_set,并在该函数中再实际调用函数__delete__

使用惯例
  我们再来看看描述符的定义:

  如果一个类(如Descr)中存在__get__、__set__和__delete__三种特殊方法的任意组合,那么该类的实例就是一个描述符

  从排列组合的层面计算,总共有 7 种合法的描述符;但从实用的角度考虑,常见的是以下三种描述符(当然也不排除您可能的应用创新:-)):


  • 只定义了__get__(非数据描述符)
  • 定义了__get__和__set__(数据描述符)
  • 定义了__get__、__set__和__delete__(也是数据描述符)

六、简单自测
  上面关于属性访问的全部细节,您是否真的懂了?观察下面的现象,尝试解释其中的原因:

# 现象1
>>> class Descr(object):
...     def __delete__(self, instance):
...         pass
...
>>> class A(object):
...     attr = Descr()
...     def __init__(self):
...         self.attr = 'why'
...
>>> a = A()
Traceback (most recent call last):
File "", line 1, in
File "", line 4, in __init__
AttributeError: __set__
# 现象2
>>> class Descr(object):
...     def __set__(self, instance, value):
...         pass
...
>>> class A(object):
...     attr = Descr()
...
>>> a = A()
>>> a.attr = 'why'
>>> del a.attr
Traceback (most recent call last):
File "", line 1, in
AttributeError: __delete__
# 现象3
>>> class Descr(object):
...     def __get__(self, instance, owner):
...         return 'why'
...
>>> class A(object):
...     attr = Descr()
...     def __init__(self):
...         self.attr = Descr()
...
>>> a = A()
>>> a.attr

>>> A.attr
'why'

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-58753-1-1.html 上篇帖子: python学习——大文件分割与合并 下篇帖子: [译]Python中有效的字符串合并方法
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表