描述器的简介
一言以蔽之,python 的描述器是一个实现了指定方法(至少包含__get__(self, obj, objtype)
)的类 A 的实例化对象 a。当这个对象是作为另一个类 B 的属性(注意是类属性而非对象属性,也即存储在 B.__dict__
内)出现,同时我们通过 B.a
或 B 的实例对象 b.a
来访问该属性时,会自动转化为调用a.__get__(b,B)
。以上这一句话就已经总结完了描述器的框架和基本协议,这一调用的转化也是一切相关魔术的核心。其之所以难以直观理解,就在于这一转化是很 artificial 的,也谈不上怎么从语言最朴素的角度去自然地理解;只不过是官方实现的有些用途,额外定义的小轮子罢了。用这个小轮子,可以进一步实现 property 装饰器,类方法和静态方法等工具。
描述器的实现
一个描述器类必须实现__get__(self, obj, objtype=None)-->value
,如果只定义了这个方法,被称为非数据的描述器,因为这样的描述器只可以取数据,无法修改或添加数据,通常是用于提供方法属性的 access。此外如果还定义了 __set__(self, obj, value)-->None
,则被称为数据描述器,这种描述器可以对数据进行取出和修改,通常用于提供数据属性的 access。此外还可以实现 __delete__(self, obj)-->None
用于实现属性的删除。
注意:这里的三个方法,千万别和默认使用用来取出类属性的 __getattribute__()
,以及出现 AttributeError 时的取属性的默认备胎方法 __getattr__()
混淆。这里的描述器,不是在实现自己这个类怎么取属性改属性这件事,而是要给别的类用的。修改 __getattribute__()
之类的,是在 hack 这个类本身,这样的魔术和例子只和这个类有关。而描述器则是定义了一个类,用来一劳永逸地 hack 其他类,描述器的使用必须有两个不同的类才能实现功能。
数据描述器和非数据描述器定义上的区别是有无实现 __set__
,其行为上的区别是,对于数据描述器,某个其他类的实例获取属性时,该描述器会优先于实例__dict__
中的属性返回。非数据描述器则反之。那如果不想允许数据修改,又想描述器覆盖类的默认属性怎么办,实现一个raise Attribute Error
的__set__
方法就好了。一个仅仅用来报错的 __set__
方法也足以告诉 python 这是个数据描述器了。关于这种优先级的区别,请看以下例子:
class nondataD(): # 非数据描述器
def __get__(self, obj ,type=None):
return 100
class dataD(nondataD): # 数据描述器
def __set__(self, obj, value):
raise AttributeError
class Test():
a = dataD()
b = nondataD()
test = Test()
test.__dict__={'a':1,'b':2}
test.a, test.b #(100,2)
实现原理上,当任意类或实例的属性被访问,而该属性恰为某个描述器类的实例时 (这里还是要注意访问顺序,对于数据描述器,定义为类属性的描述器会覆盖同名的实例属性,而对于非数据描述器,实例属性会覆盖类中的同名描述器),对于该属性的访问将直接被映射为该描述器的__get__
方法,也即对于对象属性,我们有 b.x == type(b).__dict__['x'].__get__(b,type(b))
;而对于类属性的访问,我们有 B.x == B.__dict__['x'].__get__(None,B)
。归根结底,都是根据优先顺序,直到排查到对应名称的类属性,发现是描述器实例,返回描述器的 __get__()
方法,并将类和实例作为自变量输入。这些映射都是在默认的类方法和对象方法__getattribute__()
里实现的。所以如果改写了某个类默认的 __getattribute__
方法,描述器魔术和基于描述器的很多 trick 就无法起作用了。
property 装饰器
property 是描述器的一个典型应用。我们先看一下使用接口长什么样。
class Student(object):
@property
def score(self):
return self._score
@score.setter
def 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
在这个例子中,我们为了防止其他人随意修改或指定不合理的分数,把分数这一个单纯的数据属性给封装了起来,但如果定义成函数get_score, set_score
来进行 score 的获取和修改的话,又显着有点麻烦。因此我们引入了 property,以上代码的好处是,可以直接使用 student.score
来获取和修改分数,同时不满足条件的赋值还可以报错。
为了看出这是怎么实现的,首先我们把函数装饰器先展开,以上代码大概长这样 (赋值检查部分省略):
class Student(object):
def score(self):
return self._score
score = property(score)
def score(self, value):
self._score = value
score = score.setter(score)
然后我们看一下 property 这个类的定义(注意真实的 python 是用 C 实现的,这里只是一个 python 版本的演示)
class Property():
"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
if doc is None and fget is not None:
doc = fget.__doc__
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 类里定义了 __get__
等方法,其实质是一个数据描述器。下面我们结合 property 类的定义和接口,来看其如何实现对其他实例属性的封装而调用接口不变的。
首先,score = property(score)
这一行,也就是装饰器本身,初始化了一个 property 类的实例,同时初始化了该实例的 self.fget=score
这一实例属性。这样当我们访问 student.score
时,由于数据描述器的优先级高,我们将被映射到 score.__get__(student, Student)
,而根据该函数定义,这又将返回 self.fget(student)
,也即 score(student)=student.score()
。注意这一段中,出现的 score
的意义,其中的一些代表描述器,而另一些代表原始定义的 score 函数,千万要区分清楚。
下面我们再来看进一步地属性赋值的实现。首先学生类定义时,我们有 score = score.setter(score)
,首先看括号中的 score,代表了我们刚定义的赋值的 score 函数。而等号右边的 score 则代表了刚刚生成的描述器实例,那么调用该实例的 setter()
方法的效果,可以在 property 类的定义中找到,也即生成了一个新的 property 实例,只不过此时实例的 self.fset
属性被额外定义为了这个赋值函数,因此等号左侧的 score 就代表了这个新的包含了 fset 方法的 property 实例。此时我们对 student.score = 10
赋值时,系统发现 score 其实是类属性中的一个数据描述器,由此会调用 score.__set__(student, 10)
,根据 property 类的定义,这将返回 self.fset(student, 10) = score(student,10)
,注意此时等号右边的 score 代表了我们在 Student 类中定义的赋值的那个 score 函数。
通过上面分析,我们发现逻辑倒是不难,难的是叫同一个名字 score 的东西太多了,理解时很容易发生混淆。只要明确区分出上面每个 score 的含义,property 的原理就很清楚了。
为了不这么混淆,我们也可以看下官方给出的一个 property 使用实例,这个例子和烂大街的装饰器型的用法看起来不同。如果这个例子可以完全理解,那么 property 就算知其然也知其所以然了。事实上从 property 实现上梳理,下面这种做法,反而更不容易混淆。
class C():
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
类方法和静态方法
类中不同类型的方法的实现,其背后的原理也是描述器机制。这里用到的则是非数据型的描述器。下面是类中不同类型方法应该实现的接口,我们将看到如何通过描述器来实现。
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) |
对于静态方法,只不过是“偶然”定义在类内的普通方法,无论是通过类还是实例对象使用,都返回一个普通函数,而实例或类不会作为该函数的输入。其一个 python 版本的假想实现如下:
class StaticMethod():
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
其使用接口与 property 类似,对应函数上加一个 StaticMethod 的函数装饰器即可。这样对应的函数名被一个描述器的实例所占据,该实例的 f 属性为对应的原函数。无论是通过类还是实例来取得该方法时,由于名称已被该描述器实例占据,因此将映射到该实例的__get__
方法,也即原来定义的函数方法,而类和实例的信息都不会进入自变量。
相应的,类方法无论是从类还是实例获取,第一个自变量都将传入类信息,其实现为:
class ClassMethod():
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
使用接口,同样是对指定修饰函数加 ClassMethod 的装饰器即可。这样该函数通过类或对象被取得时,将自动替换为上面类方法描述器的__get__
方法,而这一方法,确保了类会作为原函数的第一个参量默认传入。
事实上,普通的对象方法,调用时对象作为第一个参数默认传入这一点,也是类似的描述器实现的。可能有人会问,那为何普通的对象方法不需要加一个@ObjectMethod
的装饰器就能用,答案就是,对象方法的描述器直接实现在了函数这一类里。对应的模拟代码如下:
class Function():
'''
of course, there are many other attrs omitted here
'''
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return types.MethodType(self, obj)
通过上面的代码,我们也可以知道a.method(value)==A.method(a,value)
的原理。
参考文献
EOF