Python基础:新式类的属性访问
一、概述
自从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异常
参考源码
示例验证
# 步骤8:不存在属性attr,抛出AttributeError异常
>>> class A(object): pass
...
>>> a = A()
>>> a.attr
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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
参考源码
示例验证
# 步骤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 "<stdin>", line 1, in <module>
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
参考源码
示例验证
请举一反三
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
参考源码
示例验证
请举一反三
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 "<stdin>", line 1, in <module>
File "<stdin>", 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 "<stdin>", line 1, in <module>
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
<__main__.Descr object at 0x8c483ec>
>>> A.attr
'why'
Python基础:新式类的属性访问的更多相关文章
- python基础===新式类与经典类
首先: Python 2.x中默认都是经典类,只有显式继承了object才是新式类 Python 3.x中默认都是新式类,不必显式的继承object 这两种类的区别: 新式类重定义的方法更多,当然这不 ...
- python基础--新式类实现单例模式
在网上看了有关python实现单例模式的博客,发现好多都是转载的,并且都是按照python2.x版本旧式类的方式写的. 虽然也能读懂,但对于我这种一开始学的就是python3.x的新手来说,心里总有点 ...
- python基础(26):类的成员(字段、方法、属性)
1. 字段 字段:包括普通字段和静态字段,他们在定义和使用中有所区别,而最本质的区别是内存中保存的位置不同. 普通字段属于对象 静态字段属于类 字段的定义和使用: class Province: # ...
- 二十六. Python基础(26)--类的内置特殊属性和方法
二十六. Python基础(26)--类的内置特殊属性和方法 ● 知识框架 ● 类的内置方法/魔法方法案例1: 单例设计模式 # 类的魔法方法 # 案例1: 单例设计模式 class Teacher: ...
- Python的类实例属性访问规则
一般来说,在Python中,类实例属性的访问规则算是比较直观的. 但是,仍然存在一些不是很直观的地方,特别是对C++和Java程序员来说,更是如此. 在这里,我们需要明白以下几个地方: 1.Pytho ...
- python基础——枚举类
python基础——枚举类 当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份: JAN = 1 FEB = 2 MAR = 3 ... NOV = 11 DEC = 12 好处是简单 ...
- python基础——定制类
python基础——定制类 看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的. __slots__我们已经知道怎么用了,__len__()方 ...
- Python 面向对象之一 类与属性
Python 面向对象之 类与属性 今天接触了一下面向对象,发现面向对象和之前理解的简直就是天壤之别,在学Linux的时候,一切皆文件,现在学面向对象了,so,一切皆对象. 之前不是一直在学的用面向函 ...
- Python基础-类的探讨(class)
Python基础-类的探讨(class) 我们下面的探讨基于Python3,我实际测试使用的是Python3.2,Python3与Python2在类函数的类型上做了改变 1,类定义语法 Python ...
随机推荐
- 无须任何软件配置iis+ftp服务器图文说明
1.1 检查是否安装已安装IIS6组件 在windows service 2003 操作系统中,windows组件“IIS6.0”是用户搭建站点以及ftp文件共享的服务器. 具体检查步骤如下: 进入“ ...
- leetCode191/201/202/136 -Number of 1 Bits/Bitwise AND of Numbers Range/Happy Number/Single Number
一:Number of 1 Bits 题目: Write a function that takes an unsigned integer and returns the number of '1' ...
- Visual Studio 2010配置OpenGL-1.8
参考博客 : 安装参考 1. http://blog.csdn.net/mooncircle/article/details/5545448 2. http://www.cnblogs.com/moo ...
- [原创]实现android知乎、一览等的开场动画图片放大效果
代码下载地址: https://github.com/Carbs0126/AutoZoomInImageView 知乎等app的开场动画为:一张图片被显示到屏幕的正中央,并充满整个屏幕,过一小段时间后 ...
- spring中@param和mybatis中@param使用差别
spring中@param /** * 查询指定用户和企业关联有没有配置角色 * @param businessId memberId * @return */ int selectRoleCount ...
- 云计算相关的一些概念Baas、Saas、Iaas、Paas
BaaS(后端即服务:Backend as a Service)公司为移动应用开发者提供整合云后端的边界服务. SaaS(软件即服务:Software as a Service)提供了完整的可直接使用 ...
- java-cef系列视频第三集:添加flash支持
上一集我们介绍了如何搭建java-cef调试环境. 本视频介绍如何给java-cef客户端添加flashplayer支持 第四集视频我们将介绍java-cef中的自定义协议. 本作品采用知识共享署名- ...
- saiku执行速度优化二
上一篇文章介绍了添加filter可以加快查询速度.下面继续分析: 下面这个MDX语句: WITH SET [~FILTER] AS {[create_date].[create_date].[--]} ...
- 专访Linux嵌入式开发韦东山操作系统图书作者--转
CSDN学院讲师韦东山:悦己之作,方能悦人 发表于2015-04-28 08:09| 6669次阅读| 来源CSDN| 24 条评论| 作者夏梦竹 专访Linux嵌入式开发韦东山操作系统图书作者 摘要 ...
- android SDK Manager 上载失败
android SDK Manager 下载失败如题,利用android SDK Manager 无法下载各个版本的SDK,是最近无法连接上谷歌的服务器吗?我用了网上说的在C:\WINDOWS\sys ...