写在篇前
在之前的博客Python面向对象、魔法方法中曾简单提到魔法方法__get__
、__set__
、__delete__
,但只给出一个例子,这篇文章将对它做一个更详细的总结,因为这三个魔法函数表征着一个专业术语描述器。
什么是描述器
简而言之,如果一个类中定义了__get__
、__set__
、__delete__
中的任意一个,这个类的实例就可以叫做一个描述器,其功能强大,应用广泛,它可以控制访问属性、方法的行为,是属性、方法、静态方法、类方法、super函数背后的实现机制。
首先看一个例子,说明什么是描述器:
# 例一
class A():
"""
描述器类
该描述器的实现改变了 name属性 默认的存取行为
这种类是当做工具使用的,不单独使用
"""
def __init__(self):
print('descriptor init')
self.name = 'descriptor'
def __get__(self, instance, owner):
print('descriptor get')
return self.name
def __set__(self, instance, value):
if isinstance(value, str):
self.name = value
return
raise TypeError('name must be string type')
class B(object):
name = A()
def __init__(self):
self.name = 'common instance'
>>> b = B()
>>> b.name
>>> b.name = 'jeffery'
# 输出
descriptor init
descriptor get
common instance
上面代码中,A
类实现了__get__
和__set__
方法,因此是一个描述器类,B
类的类属性name
就是一个描述器。这里我们首先需要知道程序是如何访问到name描述器的?以上述代码中b
对象为例,类属性的访问过程是:
- 查找
b.__dict__
里面是否存在name
- 查找
type(b).__dict__
里面是否存在name
- 查找super(b)中是否存在属性
name
- 若存在,返回值;若都不存在,抛出异常
此时,不知道你有没有疑问,为什么最后得到的值是类属性的值?猜想一下的话,最可能的就是描述器覆盖了实例属性。但是,是不是所有描述器都会’覆盖’实例属性呢?答案是否定的,因为描述器分为以下两类:
-
资料描述器
同时定义了
__get__
和__set__
方法的描述器 -
非资料描述器
只定义了
__get__
的描述器
另外,还有一种称为 只读描述器,其实现
__get__
和__set__
,但是__set__
函数抛出AttributeError
,但是这同时也是一个资料描述器。
它们存在潜在优先级关系:资料描述器 > 实例属性 > 非资料描述器;实例属性 > 类属性,请看以下实例:
# 例子二
class A:
def __init__(self):
self.x = 1
def __get__(self, instance, owner):
"""
以下两个参数都是「必须参数」,约定使用
instance: 描述器所在类的实例
owner:调用描述器的类
"""
return self.x
def __set__(self, instance, value):
"""
以下两个参数都是「必须参数」,约定使用
instance: 描述器所在类的实例
value:用来设置属性的值
"""
self.x = value
def __delete__(self, instance):
"""
以下参数是「必须参数」,约定使用
instance: 描述器所在类的实例
"""
pass
class B:
def __init__(self):
self.x = 1
def __get__(self, instance, owner):
return self.x
class C:
a = A()
b = B()
def __init__(self, a, b):
self.b = a
self.b = b
上面在class C
中,分别定义了资料描述器和非资料描述器a,b
以及同名的实例属性,进行以下输出测试,发现实例c
只存在实例属性b
,不存在实例属性a
,说明其确实是被资料描述器,即类属性a
给屏蔽了,从而证实了优先级关系:资料描述器 > 实例属性 > 非资料描述器。
>>> c = C(7,8)
>>> c.__dict__
{'b': 8}
关于资料描述器、非资料描述器的调用,可用如下方式:
# 资料描述器
>>> c.a
1
>>> type(c).__dict__['a'].__get__(c,C)
1
# 非资料描述器
>>> C.b
1
>>> type(c).__dict__['b'].__get__(c,C)
1
描述器调用原理
为了探究描述器的底层调用原理,我们借用例一中定义的class A
和 class B
,实际上如果我们直接初始化一个class A
对象,那么其调用方式很显然是:
>>> a = A()
>>> a.__get__(None,None)
那class B
实例中的描述器是如何调用的呢?实际上,python是通过__getattribute__()
函数来实现的,但它的使用C语言实现的,如果用python来写的话,类似于下面的代码:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
也就是说,当在字典中找到一个值之后,会判断他是不是一个描述器,如果是的话,就调用__get__
方法,从而实现描述器的访问功能。如果你重写__getattribute__()
函数,那么就可以改变对描述器访问的行为,如下面例子所示:
class A():
def __init__(self):
self.name = 'descriptor'
def __get__(self, instance, owner):
return self.name
def __set__(self, instance, value):
raise TypeError('name must be string type')
class B(object):
name = A()
def __getattribute__(self, item):
if item in type(self).__dict__:
if hasattr(type(self).__dict__[item], '__get__'):
print('oh, be careful, it is a descriptor')
if __name__ == '__main__':
b = B()
b.name
# 输出
oh, be careful, it is a descriptor
描述器实例
在我的上一篇博客python装饰器详细剖析中,我举例分析了实例方法、类方法、静态方法、抽象方法的差异,这里我们进行深度挖掘一下其实现原理:
首先定义一个类ICU996
,用于相关测试:
import abc
class ICU996():
@staticmethod
def static_m():
print('static_m')
@classmethod
def class_m(cls):
print('class_m')
def instance_m(self):
print('instance_m')
@abc.abstractmethod
def abstract_m(self):
print('abstract_m')
下面代码分别使用属性访问和字典访问的方式,来看看各类函数的差异。可以看出,实例对象直接调用时,类方法、抽象方法、实例方法都是bound method
,而静态方法是function
。值得注意的是,静态方法和类方法用字典调用得到的其实分别是staticmethod和classmethod两个类的对象,这两个类其实是定义描述器的类,所以用字典访问的两个方法得到的都是描述器对象。
icu = ICU996()
print(icu.static_m)
print(ICU996.__dict__['static_m'])
print(icu.class_m)
print(ICU996.__dict__['class_m'])
print(icu.instance_m)
print(ICU996.__dict__['instance_m'])
print(icu.abstract_m)
print(ICU996.__dict__['abstract_m'])
# 输出
<function ICU996.static_m at 0x11a9cff28>
<staticmethod object at 0x10edcd208>
<bound method ICU996.class_m of <class '__main__.ICU996'>>
<classmethod object at 0x10edcd320>
<bound method ICU996.instance_m of <__main__.ICU996 object at 0x10edcd2b0>>
<function ICU996.instance_m at 0x11a9f60d0>
<bound method ICU996.abstract_m of <__main__.ICU996 object at 0x10edcd2b0>>
<function ICU996.abstract_m at 0x11a9f6158>
既然它们是描述器对象,那么调用就可以使用__get__
函数来实现;另外,实际上其他函数也都是通过描述器实现的(通过非资料描述器实现的),只不过其他方法应该传入一个self
参数,不妨试一试:
ICU996.__dict__['class_m'].__get__(None, ICU996)
ICU996.__dict__['static_m'].__get__(None, ICU996)
ICU996.__dict__['instance_m'].__get__(icu, ICU996)
ICU996.__dict__['abstract_m'].__get__(icu, ICU996)
property
因为property
的实现是用C语言实现的,我们可以看一个等效的python实现,大致是这样的:
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
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
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"
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "can't delete attribute"
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__)
举个使用的示例,通过property
类来实现属性代理,其效果与@property示例等效,但是重点是要去思考,为什么等效?
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
def del_score(self):
del self._score
score = property(get_score, set_score, del_score, 'doc string')
接着上面的问题,为什么等效?是怎么调用的,思考一下描述器原理,不难想出调用的过程就是:
>>> s = Student()
>>> s.score = 10
>>> type(s).__dict__['score'].__get__(s, type(s))
Functions&methods
Python的面向对象特征是建立在基于函数的环境之上的,非资料描述器把两者无缝地连接起来。为了支持方法调用,函数包含一个 __get__()
方法以便在属性访问时绑定方法。这就是说所有的函数都是非资料描述器,它们返回绑定(bound)还是非绑定(unbound)的方法取决于他们是被实例调用还是被类调用。借用上文定义的class ICU996
,调用一个方法的实际过程是:
>>> icu = ICU996()
>>> ICU996.__dict__['instance_m'].__get__(icu, ICU996)()
因此可以猜测到,__get__()
函数的实现方式如下面代码所描述。但是可能依旧会有疑问,就算真的存在这样一个描述器,我们定义上面的instance_m
方法并没有像使用property
描述器一样显示调用呀?怎么就起作用了呢?原因很简单,隐式的调用了!那在哪里调用了呢?我的猜想是,一切皆为对象,函数也是对象,那么函数也就必然属于一个类,比如他叫Function,Function类实现了如下的代码,也就是函数就是一个描述器,因此通过以下__get__
方法实现方法到对象的绑定。
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
staticmethod&classmethod
其实类似的,通过实现__get__
方法,使其变成一个描述器,下面分别实现以下classmethod和staticmethod:
class myclassmethod(object):
def __init__(self, method):
self.method = method
def __get__(self, instance, cls):
return lambda *args, **kw: self.method(cls, *args, **kw)
class myclassmethod(object):
def __init__(self, method):
self.method = method
def __get__(self, instance, cls):
return self.method
小结
一般方法、静态方法、类方法的调用转换可以归结为下表:
Transformation | Called from an Object | Called from a Class |
---|---|---|
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |