Python技术进阶——描述器

版权声明:本文为博主原创文章,未经博主允许不得转载。

前言

在Python开发中,我们很少能接触到描述器的直接使用,但对于熟练使用Python的开发者,了解Python描述器的工作原理,能让你更深入地了解Python以及其设计的优雅之处。

其实我们开发中遇到的很多例子,例如:

  • 装饰器propertystaticmethodclassmethod
  • functionbound methodunbound method

是不是都很熟悉,其实这些都与描述器有着千丝万缕的关系,这篇文章就为大家一一解答其中的奥秘。

什么是描述器?

一般来说,描述器是一个有「绑定行为」的对象属性,它的访问控制被描述器协议方法重写。

回忆一下,在编程中我们说「行为」一般指的是方法。

那么这就容易理解了,「绑定行为」的对象属性,就是指这个「对象属性」依托于了另外的对象,这个对象里包含了很多「方法」来控制这个行为。

描述器协议

对象属性依托的对象中,包含的「方法」不能随便定义,而是规定好的,实现这些方法,就是实现了描述器协议,具体有以下几个:

  • __get__
  • __set__
  • __delete__

只要实现以上方法其一,这个对象就叫做描述器。

  • 定义了__get____set__的对象叫做资料描述器
  • 只定义了__get___的对象叫做非资料描述器

这两者有什么区别呢?我们下面再做解释。

描述器的调用

我们来看一段描述器的代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
class Age(object):
"""这个类实现了描述器协议"""
def __init__(self, value=20):
self.value = value
def __get__(self, obj, type=None):
print 'get --> obj: %s type: %s' % (obj, type)
return self.value
def __set__(self, obj, value):
print 'set --> obj: %s value: %s' % (obj, value)
self.value = value
class Person(object):
age = Age() # 这个属性通过描述器托管给了另一个类
def __init__(self, name):
self.name = name
person = Person('zhangsan')
print person.age
# get --> obj: <__main__.Person object at 0x105e815d0> type: <class '__main__.Person'>
# 20
print Person.age
# get --> obj: None type: <class '__main__.Person'>
# 20
person.age = 25
# set --> obj: <__main__.Person object at 0x105e815d0> value: 25
print person.age
# get --> obj: <__main__.Person object at 0x105e815d0> type: <class '__main__.Person'>
# 25

我们从代码看出,Person类的类属性ageAge实现,Age类实现了__set____get__方法,对于Person类来说,age就是一个描述器

我们通过输出结果看出,当调用age属性时,都调用了Age__get__方法,但打印出的参数结果不同:

  • 当调用方是实例时,objPerson实例,typetype(Person)
  • 当调用方是时,objNonetypetype(Person)

工作原理

这背后的机制到底是怎样的呢?

要解释这个现象,我们要先从属性被访问的机制来说,调用a.b会发生什么?

如果a的类是继承了object,也就是说这个类是新式类,那么a.b会调用__getattribute__方法,如果类中没有定义这个方法,那么默认会调用object__getattribute__方法。

object__getattribute__中就默认调用了描述器,但调用细节取决于a是一个类还是一个实例:

  • 如果a是一个实例object的这个方法会把其变为:
1
type(a).__dict__['b'].__get__(a, type(a))
  • 如果a是一个object的这个方法会把其变为:
1
a.__dict__['b'].__get__(None, a)

所以我们就能看到上面例子输出的结果。

也就是说,描述器的调用入口,取决于__getattribute__

如果我们重写了__getattribute__,那么会阻止描述器的调用。

方法就是描述器

我们思考一个问题,当一个类中的实例变量名与一个方法同名时,例如类A有一个实例变量和方法都叫foo,那么A().foo会输出实例属性还是调用方法?

1
2
3
4
5
6
7
8
9
class A(object):
def __init__(self):
self.foo = 'abc'
def foo(self):
return 'xyz'
print A().foo # abc

我们看到,A().foo输出了abc,也就是实例属性的值,而不是调用这个方法,这是怎么回事?

我们执行如下代码:

1
2
3
print dir(A.foo)
# ['__call__', '__class__', '__get__', '__delattr__', '__doc__', ...

看到了吗?dir(A.foo)包含了__get__方法,我们在上面知道描述器的定义是:只要实现了__get____set____del__其一,这个对象就是描述器。

也就是说:方法就是一个描述器,而且是一个非资料描述器

其实在调用一个属性时,具体的执行顺序是这样的:

1
__getattribute()__ -> 资料描述器 > 实例变量 > 非资料描述器 > __getattr()__

当一个类中的实例变量名与一个方法同名时:

  • 如果描述器是资料描述器,优先使用资料描述器
  • 如果描述器是非资料描述器,优先使用字典中的属性

由于每个方法都是一个非资料描述器,所以优先使用实例变量。

到这里我们可以总结一下:

  • __getattribute__只对新式类的实例有用
  • 描述器的调用是因为object__getattribute__
  • 重写__getattribute__方法会阻止正常描述器的调用
  • 方法都是非资料描述器
  • 实例和类调用__get__结果不一样
  • 资料描述器 > 实例变量 > 非资料描述器调用

function/unbound method/bound method

我们常见的functionunbound methodbound method有什么区别呢?

1
2
3
4
5
6
7
8
class A(object):
def foo(self):
return 'xyz'
print A.__dict__['foo'] # <function foo at 0x10a790d70>
print A.foo # <unbound method A.foo>
print A().foo # <bound method A.foo of <__main__.A object at 0x10a793050>>

它们的区别如下:

  • function就是一个函数,因为其实现了__get__,因此每个函数都是一个非资料描述器
  • 类的字典把方法当做函数存储
  • 当方法被实例调用时,返回绑定的方法(bound method)
  • 当方法被类调用时,返回非绑定的方法(unbound method

property/staticmethod/classmethod

Python把一些使用特别普遍的功能打包成了独立的函数,例如propertystaticmethodclassmethod,这些方法都是基于描述器协议实现的。

property的Python版实现:

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
27
28
29
30
31
32
33
class property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self.fget
if self.fget is None:
raise AttributeError(), "unreadable attribute"
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError, "can't set attribute"
return self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "can't delete attribute"
return self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

staticmethod的Python版实现:

1
2
3
4
5
6
7
class staticmethod(object):
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
return self.func

classmethod的Python版实现:

1
2
3
4
5
6
7
8
9
10
11
class classmethod(object):
def __init__(self, func):
self.func = func
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.func(klass, *args)
return newfunc

由此可见,通过描述符我们可以实现强大而灵活的属性和方法管理,但强大也意味着责任大,在合适的场景使用才能起到最佳的效果。


如果此文章能给您带来小小的工作效率提升,不妨小额赞助我一下,以鼓励我写出更好的文章!
kaito-kidd WeChat Pay

微信打赏

kaito-kidd Alipay

支付宝打赏