在 python 引入新式类的同时,引进了很多新的概念,其中描述符就是一个。我个人认为描述符的概念有点难以理解,但是,却很重要,可以说是 python 新式类中属性和方法的核心要素,这篇文章就以个人的视角介绍一下描述符。

简介

描述符是 Python 新式类中的核心关键之一,它为新式类的对象属性和方法提供了强大的 API。那么描述符到底是什么呢?我认为描述符是表示对象属性的一个代理。

一个 property 的例子

和其他的人介绍描述符不一样,我首先先介绍一个例子,一个用 property 作为属性的例子,例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)

执行一遍可以看到输出是:

1
2
3
ser value [123] for x
get value of x
123

如果你对 property 不熟悉的话,这里解释一下,从输出可以看出,对属性 x 设置值的时候调用的是 set_x 的函数,注意,这里说的是函数,不是方法;而获取属性 x 值的时候调用的是函数 get_x。从这里我们可以看出一点概念,之前所说的描述符是表示对象属性的一个代理,其实就是说操作 x 并不是真的操作了一个属性,而是操作的一些函数。

说回描述符

可以发现,之前定义 property 的时候是传了两个参数,其实 property 的声明是这样的:

1
property(fget=None, fset=None, fdel=None, doc=None)

也就是说有三个函数可以传递,此外还有一个 doc。从第一个例子可以发现因为是函数,所以如果我们需要使用 property 定义一个属性的话,可能需要写三个函数,这样的话代码的结构会有点差,那么既然是新式类,有没有类的写法,答案是显然有的,我们可以这么玩:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

这里其实我们就有一点自己定义了一个类型的意思了,其实发现描述符就是一个类,只不过这个类比较特殊,必须实现

中的一个或多个,这里有一个说法:

需要注意的一点就是方法/函数都是非数据描述符。

优先级别

那么区分数据描述符和非数据描述符有什么用?用处就是在访问对象的属性的时候,__getattribute__() 方法是以以下优先级进行访问数据的:

那么这个优先级有什么用?其中一个用处就是当在实例中有同名的属性和方法时,因为方法是非数据描述符,所以,我们访问的肯定是属性,而不是方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

这段程序的结果应该是:

1
2
123
<bound method Test.x of <__main__.Test object at 0x7f7b06707b90>>

__getattribute__ 方法

上面说了一下 __getattribute__ 是按优先级调用的,但是__getattribute__ 是如何调用的 __get__ 方法的呢?

对于一个类 X 和实例 x,我们调用 x.foo 由 __getattribute__ 转化成:

1
type(x).__dict__['foo'].__get__(x, type(x))

如果调用的是 X.foo 的话,那应该是:

1
X.__dict__['foo'].__get__(None, X)

Reference