Python descriptor
Contents
一次偶然发现,Python的对象竟然可以在运行期动态添加类定义时没有的属性,这又颠覆了我对Python OO机制的理解。Google了一把,顺着__dict__
属性一路找到descriptor,揭开了隐藏在Python对象之后的内幕。
本文主要记录Python的descriptor机制,以及其在Python对象的属性、方法绑定上的作用。
先从本文的始作俑者,运行期动态添加对象属性开始讲起。
|
|
以上代码奇迹般的没有报错,而且还输出了1。这肯定会让写过C++/Java代码的童鞋表示吃惊,Python变量类型动态也就不稀奇了,对象属性还能动态添加的?Python到底在背后做了什么?
神奇的__dict__
在a.attr = 1
前后分别加上一行print(a.__dict__)
就会得到如下结果:
|
|
显而易见,我们在运行期定义的属性和类定义时定义的属性都被放在了__dict__
里。
到这里有人可能就有疑问了,Python里的一切不都是对象麽?为什么成员函数__init__
、f
不在这个字典里?
看看A.__dict__
里有什么就明白了:
|
|
这时才恍然大悟,如果成员变量看做是对象的属性,那么成员函数就应该看成是类的属性,被全部对象共享嘛。
更精确地讲,以object.attribute
访问一个对象的属性时,属性的搜索顺序为:
- 对象自身,
object.__dict__
- 对象类型,
object.__class__.__dict__
- 对象类型的基类,
object.__class__.__bases__
中的所有__dict__
。注意,当多重继承的情况下有菱形继承的时候,Python会根据MRO确定的顺序进行搜索。关于MRO(Method Resolution Order)是什么有时间专门写一篇文章总结一下。
当以上三个步骤都没有找到要访问的属性的时候Python就只能抛出AttributeError
异常了。
Descriptor是什么
讲了这么多,貌似跟descriptor半毛钱关系没有嘛。别急,接着往下看。
|
|
来测试一下这个类
|
|
这个RevealAccess
的对象就是一个descriptor,其作用就是在存取变量的时候做了一个hook。访问属性m.x
就是调用__get__
方法,设置属性值就是调用__set__
方法。还可以有一个__delete__
方法,在del m.x
时被调用。
只要一个类定义了以上三种方法,其对象就是一个descriptor。我们把同时定义__get__
和__set__
方法的descriptor叫做data descriptor,把只定义__get__
方法的叫non-data descriptor
Method binding
有了以上两个概念,我们就能讨论Python的方法绑定了。
还记得讨论__dict__
时的成员函数f
吗?按照我们的推测,A.__dict__['f']
应该和a.f
是一个东西。但是!!!
|
|
这两个显然不是一个东西,一个是function
,一个是bound method
。这是什么情况?淡定,看下面
|
|
这下放心了吧:D
其实,类的成员函数就是一个descriptor,在实例化对象a的时候,Python就做了这么一个过程(伪码,详见Objects/funcobject.c):a.f = A.__dict__['f'].__get__(a, A)
纯Python模拟的函数对象就像这样:
|
|
然后就好理解staticmethod
和classmethod
这两个decorator了吧。staticmethod
无视了传入的第一个self参数,classmethod
手工加了一个类对象参数进去。它们的纯Python模拟就像下面所示:
|
|
研究Python的底层实现是个很有意思的事,至少能让我在使用Python时更加放心:)
全文完
参考资料:
- How-To Guide for Descriptors: http://users.rcn.com/python/download/Descriptor.htm
- Python Attributes and Methods: http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html
- 《Expert Python Programming》,Tarek Ziadé: http://book.douban.com/subject/3285148/