摘要
明确解释描述符,概述描述符协议,展示描述符如何调用。详细阐述自定义描述符和一些内建Python描述符如函数、属性、静态方法、类方法,通过对比和示例展示它们的工作运行情况。
学习描述符,其不仅提供访问较大工具集的功能,而且通过学习,对Python运行原理的理解将会更加深入,并会欣赏到Python设计的优雅。
明确解释与介绍
一般来说,描述符被认为是绑定了行为的对象属性,对象属性的行为是被具有描述符协议的方法重新定义的。这些具有描述符协议的方法为__get__,__set__,还有__delete__,如果某个对象属性定义了其中任何一个方法,就可称其为描述符。
属性默认的行为是从对象字典表中get,set,或者delete对象的属性。例如,a.x是在a的字典表中查找a.__dict__['x'],然后是type(a).__dict__['x'],接着继续扫遍metaclass以外的type(a)的基础类。如果要查找的值是一定义了某个描述符方法的对象,Python将会以描述符方法覆盖其默认行为,而这发生在优先级链的哪个位置依赖于哪个描述符方法被定义。注意,描述符只在使用新式对象或类时被调用(如果类继承了object或type,则其为新式类)。
描述符是强大的通用的协议,它是Python属性、方法、静态方法、类方法、super内建方法得以实现的背后机制。在Python2.2中介绍新式类时使用了描述符,描述符简化了潜在的C代码并为每天的python编程提供了一套灵活的新工具。
描述符协议
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
1
2
3
descr
.
__get__
(
self
,
obj
,
type
=
None
)
--
>
value
descr
.
__set__
(
self
,
obj
,
value
)
--
>
None
descr
.
__delete__
(
self
,
obj
)
--
>
None
就是这么回事,作为属性,定义了其中任何一个方法,就会被认为是描述符,其默认行为会被改变(被改变默认行为的类属性为描述符)。
如果对象定义__get__,__set__两个方法,则被称为数据描述符,只定义了__get__方法,为非数据描述符(非数据描述符通常用在于方法,但也可能用在其他方面)。
对于实例字典的所有项来说,数据和非数据描述符的不同在于怎样执行覆盖行为。如果某实例的字典中存在与数据描述符名字相同的记录,则数据描述符的优先级较高;如果某实例的字典中存在与非数据描述符名字相同的记录,则字典项的优先级较高。
数据描述符
>>> class Descriptor(object):
... def __get__(self, obj, type):
... print "__get__"
... def __set__(self, obj, type):
... print "__set__"
...
>>> class MyClass(object):
... property = Descriptor()
...
>>> x = MyClass()
>>> x.property = "Hello World"
__set__
>>> x.property
__get__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>>
class
Descriptor
(
object
)
:
.
.
.
def
__get__
(
self
,
obj
,
type
)
:
.
.
.
"__get__"
.
.
.
def
__set__
(
self
,
obj
,
type
)
:
.
.
.
"__set__"
.
.
.
>>>
class
MyClass
(
object
)
:
.
.
.
property
=
Descriptor
(
)
.
.
.
>>>
x
=
MyClass
(
)
>>>
x
.
property
=
"Hello World"
__set__
>>>
x
.
property
__get__
非数据描述符
>>> class Descriptor(object):
... def __get__(self, obj, type):
... print "__get__"
...
>>> class MyClass(object):
... property = Descriptor()
...
>>> x = MyClass()
>>> x.property = "Hello World"
>>> x.property
'Hello World'
1
2
3
4
5
6
7
8
9
10
11
>>>
class
Descriptor
(
object
)
:
.
.
.
def
__get__
(
self
,
obj
,
type
)
:
.
.
.
"__get__"
.
.
.
>>>
class
MyClass
(
object
)
:
.
.
.
property
=
Descriptor
(
)
.
.
.
>>>
x
=
MyClass
(
)
>>>
x
.
property
=
"Hello World"
>>>
x
.
property
'Hello World'
要想使数据描述符只读,则可以定义__get__和__set__两个方法,并让__set__在被调用时抛出AttributeError异常,定义__set__方法并直接用抛出异常作为占位来构建数据描述符就可以了。
调用描述符
可以直接用方法名字来调用描述符,例如:d.__get__(obj)。
通常描述符作为属性被访问是自动调用的。例如,obj.d是在obj的字典表中查找d。此外,如果d定义了__get__方法,那么d.__get__(obj)根据上边列出的优先级法则来调用。
调用的细节主要看obj是对象还是类,但无论是哪种,描述符也仅能工作于新式类和新式对象。属于object的子类便是新式类。
对于对象来说,描述符的机制在于object.__getattribute__,它能把b.x转换成type(b).__dict__['x'].__get__(b, type(b)),而执行时依照于优先级链,使数据描述符的优先级高于实例的变量,实例变量的优先级高于非数据描述符。若有提供__getattr__,则赋予最低优先级。全部的C语言执行过程可在Objects/object.c的PyObject_GenericGetAttr()中找到。
对于类来说,描述符的机制在于type.__getattribute__,它能把B.x转换成B.__dict__['x'].__get__(None, B)。
纯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
1
2
3
4
5
6
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
需要记住的重要几点是:
描述符由__getattribute__方法调用
覆盖__getattribute__将阻止描述符的自动调用
__getattribute__只在新式类和新式对象中起作用
object.__getattribute__和type.__getattribute__对于__get__的调用是不同的
数据描述符总是覆盖实例的字典记录
非数据描述符会被实例字典记录所覆盖
1
2
3
4
5
6
描述符由 __getattribute__
方法调用
覆盖 __getattribute__
将阻止描述符的自动调用
__getattribute__
只在新式类和新式对象中起作用
object
.
__getattribute__
和 type
.
__getattribute__
对于 __get__
的调用是不同的
数据描述符总是覆盖实例的字典记录
非数据描述符会被实例字典记录所覆盖
super返回的对象也有一个定制的__getattribute__方法用于调用描述符。调用super(B, obj).m()会查询obj.__class__.__mro__来确定B的基类是A,然后返回A.__dict__['m'].__get__(obj, A)。如果不是描述符,m返回未改变之前的值。如果不在字典表中,m会重新用object.__getattribute__来查找。
注意,在Python2.2中,如果m是一个数据描述符,super(B, obj).m()将只调用__get__。在python2.3中,非数据描述符也将被调用,除非是使用了旧式经典类。执行细节在Objects/typeobject.c的super_getattro(),纯python的等价实现在 Guido’s Tutorial
从上边的内容中可以看到,对于对象、类型、super来说,描述符的机制是被嵌入到__getattribute__()方法中的,当类源于object时,它会继承这个方法,或者如果类存在一个提供相似功能的metaclass也可以。此外,类可以覆盖__getattribute__()方法来关闭描述符调用。
描述符范例
下面的代码构造了一个类,它的对象是数据描述符,会在get或者set时打印出一条消息,并且覆盖__getattribute__也可以做到这一点。但是这描述符只可以监控一小部分选定的属性:
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()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
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
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
)
:
'Retrieving'
,
self
.
name
return
self
.
val
def
__set__
(
self
,
obj
,
val
)
:
'Updating'
,
self
.
name
self
.
val
=
val
>>>
class
MyClass
(
object
)
:
x
=
RevealAccess
(
10
,
'var "x"'
)
y
=
5
>>>
m
=
MyClass
(
)
>>>
m
.
x
Retrieving
var
"x"
10
>>>
m
.
x
=
20
Updating
var
"x"
>>>
m
.
x
Retrieving
var
"x"
20
>>>
m
.
y
5
描述符的协议很简单,提供了令人兴奋的可能性,几个惯常的使用状况被封装在独特的函数调用中。属性,绑定和非绑定方法,静态方法,还有类方法,都是基于描述符协议的。
属性
执行property()方法是一个构建数据描述符的简单方法,当用property()构建的属性被访问时,将会触发相应的函数调用。它的表现形式如下:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
1
property
(
fget
=
None
,
fset
=
None
,
fdel
=
None
,
doc
=
None
)
->
property
attribute
这篇文章给出一个管理属性x的典型应用:
class C(object):
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.")
1
2
3
4
5
class
C
(
object
)
:
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."
)
要看property()是怎样按照描述符协议执行的,这里有个纯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)
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
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
)
每当在用户界面对属性进行访问时,内建函数property()就会起作用,接着随后的变化需要描述符方法的介入。
例如,某个电子表格类可能要用Cell(‘b10′).value来访问某个单元值,随后程序的变更需要此单元值在每次访问时被计算两次,可是程序员并不想影响到存在的客户端代码直接访问属性,解决方法是打包成property()数据描述符赋给value属性:
class Cell(object):
. . .
def getvalue(self, obj):
"Recalculate cell before returning value"
self.recalc()
return obj._value
value = property(getvalue)
1
2
3
4
5
6
7
class
Cell
(
object
)
:
.
.
.
def
getvalue
(
self
,
obj
)
:
"Recalculate cell before returning value"
self
.
recalc
(
)
return
obj
.
_value
value
=
property
(
getvalue
)
函数与方法
Python的面向对象特性建立在基于函数的环境里,通过加入非数据描述符,可以使两者无缝的结合起来。
类字典表存储方法作为函数。在类定义时,创建函数的通常方式是使用def和lambda作为方法,方法与常规函数仅有的不同是,方法的第一个参数传入的是对象实例,按照Python惯例,实例引用被叫做self,但也可能叫做this或者其他变量名字。
为了支持方法调用,在被访问时,函数作为属性会包含__get__来绑定方法,意思是说所有的函数都是非数据描述符,其返回绑定还是非绑定方法取决于它们是从对象中被调用还是从类中被调用。用纯Python实现如下:
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
1
2
3
4
5
class
Function
(
object
)
:
.
.
.
def
__get__
(
self
,
obj
,
objtype
=
None
)
:
"Simulate func_descr_get() in Objects/funcobject.c"
return
types
.
MethodType
(
self
,
obj
,
objtype
)
运行解释器,做下面的练习,看函数描述符怎样工作:
>>> class D(object):
def f(self, x):
return x
>>> d = D()
>>> D.__dict__['f'] # Stored internally as a function
>>> D.f # Get from a class becomes an unbound method
>>> d.f # Get from an instance becomes a bound method
>
1
2
3
4
5
6
7
8
9
10
11
>>>
class
D
(
object
)
:
def
f
(
self
,
x
)
:
return
x
>>>
d
=
D
(
)
>>>
D
.
__dict__
[
'f'
]
# Stored internally as a function
<
function
f
at
0x00C45070
>
>>>
D
.
f
# Get from a class becomes an unbound method
<
unbound
method
D
.
f
>
>>>
d
.
f
# Get from an instance becomes a bound method
<
bound
method
D
.
f
of
<
__main__
.
D
object
at
0x00B18C90
>>
输出表明,绑定和非绑定方法是两个不同的类型。在Python中两者是这样执行,但实际在功能同等的C执行中(执行PyMethod_Type in Objects/classobject.c)im_self域被设置或置为NULL会得到两种不同的结果。就是说,调用方法的结果要看im_self域的设置情况,如果设置(意味着是绑定的),存在于im_func域中的函数被调用,并且函数的第一个参数是实例,如果是非绑定的,所有参数则不传给函数而被无任何改变的被pass掉。实际的C函数instancemethod_call()包括一些类型检查,有一些复杂。
静态方法和静态类
continuing…
[original source] http://users.rcn.com/python/download/Descriptor.htm