在 python 引入新式类的同时,引进了很多新的概念,其中描述符就是一个。我个人认为描述符的概念有点难以理解,但是,却很重要,可以说是 python 新式类中属性和方法的核心要素,这篇文章就以个人的视角介绍一下描述符。
简介
描述符是 Python 新式类中的核心关键之一,它为新式类的对象属性和方法提供了强大的 API。那么描述符到底是什么呢?我认为描述符是表示对象属性的一个代理。
一个 property 的例子
和其他的人介绍描述符不一样,我首先先介绍一个例子,一个用 property 作为属性的例子,例子如下:
def get_x(obj):
print("get value of x")
return obj._x
def set_x(obj, value):
print("ser value [{}] for x".format(value))
obj._x = value
class MyClass(object):
x = property(get_x, set_x)
if __name__ == "__main__":
mc = MyClass()
mc.x = 123
print (mc.x)
执行一遍可以看到输出是:
ser value [123] for x
get value of x
123
如果你对 property 不熟悉的话,这里解释一下,从输出可以看出,对属性 x 设置值的时候调用的是 set_x 的函数,注意,这里说的是函数,不是方法;而获取属性 x 值的时候调用的是函数 get_x。从这里我们可以看出一点概念,之前所说的描述符是表示对象属性的一个代理,其实就是说操作 x 并不是真的操作了一个属性,而是操作的一些函数。
说回描述符
可以发现,之前定义 property 的时候是传了两个参数,其实 property 的声明是这样的:
property(fget=None, fset=None, fdel=None, doc=None)
也就是说有三个函数可以传递,此外还有一个 doc。从第一个例子可以发现因为是函数,所以如果我们需要使用 property 定义一个属性的话,可能需要写三个函数,这样的话代码的结构会有点差,那么既然是新式类,有没有类的写法,答案是显然有的,我们可以这么玩:
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Retrieving', self.name
return self.val
def __set__(self, obj, val):
print 'Updating', self.name
self.val = val
class MyClass(object):
x = RevealAccess(10, 'var "x"')
y = 5
m = MyClass()
print m.x
m.x = 20
print m.x
print m.y
这段代码会输出:
[root@liqiang.io]# python test.py
Retrieving var "x"
10
Updating var "x"
Retrieving var "x"
20
5
这里其实我们就有一点自己定义了一个类型的意思了,其实发现描述符就是一个类,只不过这个类比较特殊,必须实现
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
中的一个或多个,这里有一个说法:
- 如果一个对象同时定义了 get() 和 set(),它叫做数据描述符(data descriptor)
- 仅定义了 get() 的描述器叫非数据描述符(常用于方法,当然其他用途也是可以的)
需要注意的一点就是方法/函数都是非数据描述符。
优先级别
那么区分数据描述符和非数据描述符有什么用?用处就是在访问对象的属性的时候,__getattribute__()
方法是以以下优先级进行访问数据的:
- 类属性
- 数据描述符
- 实例属性(
__dict__
/__slot__
) - 非数据描述符
- 默认为
__getattr__
那么这个优先级有什么用?其中一个用处就是当在实例中有同名的属性和方法时,因为方法是非数据描述符,所以,我们访问的肯定是属性,而不是方法:
class Test(object):
def __init__(self):
self.x = 123
def x(self):
print "function x"
t = Test()
print t.x
del t.x
print t.x
这段程序的结果应该是:
123
<bound method Test.x of <__main__.Test object at 0x7f7b06707b90>>
__getattribute__ 方法
上面说了一下 __getattribute__
是按优先级调用的,但是__getattribute__
是如何调用的 __get__
方法的呢?
对于一个类 X 和实例 x,我们调用 x.foo 由 __getattribute__
转化成:
type(x).__dict__['foo'].__get__(x, type(x))
如果调用的是 X.foo 的话,那应该是:
X.__dict__['foo'].__get__(None, X)