摘要
本文定义了描述符,总结了其中的协议,并且介绍如何调用描述符。在此基础上,本文展示了一个自定义的描述符和几个内置的Python描述符,包括函数,属性,静态方法和类方法。对于这些内置的描述符,本文通过提供编写相同功能的python函数来展示其工作机制。
学习描述符不仅可以学会使用工具,还可以更深入地了解Python的工作原理,并了解其设计的优雅之处。
定义和介绍
一般来讲,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。这些方法是__get__(),__set__()和__delete__()。如果为一个对象定义了这些方法中的任何一个,那么它将被称为一个描述符。
属性访问的默认行为是从对象的字典中获取,设置或者删除属性。比如,a.x
的查找过程是:先找a.__dict__['x']
,再找type(a).__dict__['x']
,然后遍历type(a)
的基类。如果查找的值在定义了描述符方法的对象中,那么Python可以重写默认行为并调用描述符方法。在这种情况下,查找的顺序由定义的描述符方法决定。请注意,描述符仅仅对新式类(继承自object
或者type
)生效。
描述符是一个强大的通用协议。它们是属性,方法,静态方法,类方法和super()
背后的机制。新式类(Python2.2版本引入)的实现使用了描述符。描述符简化了底层的C代码,并为日常的Python程序提供了一套灵活的新工具。
描述符协议
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
这就是所有的了。对象如果定义了上面方法中的任何一个,它会被认为是一个描述符,并且可以在属性被查找时覆盖默认行为。
如果一个对象定义了__get__()和__set__(),它会被认为是一个数据描述符。仅定义__get__()的描述符被称为非数据描述符(它们通常用于方法,但可能有其他用途)。在计算实例字典如果实例的字典具有与数据描述符相同名称的条目,则数据描述符优先调用。如果实例的字典具有与非数据描述符相同名称的条目时,字典条目优先调用。
要创建只读数据描述符,需要定义__get__()和__set__(),并且在__set__()被调用时抛出AttributeError异常。
描述符的调用
描述符可以通过其方法名直接调用,比如d.__get__(obj)
。
更常见的是在访问属性时自动调用描述符。例如,obj.d
会在obj
的字典中查找d
。如果d
中定义了__get__()
方法,则根据下面列出的优先级规则调用d.__get__(obg)
。
调用的细节取决于obj
是一个对象还是一个类。
对于对象来说,其中的机制在于object.__getattribute__(),它将b.x
转换成type(b).__dict__['x'].__get__(b, type(b))
。这个方法实现了一个优先级链,在该链中,数据描述符优先级高于实例变量,实例变量优先级高于非数据描述符,如果定义了__getattr__(),那么它的优先级最低。其中的完整实现见PyObject_GenericGetAttr(),其位于Objects/object.c。
对类来说,其中的机制在于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
以下要点要记住:
- 描述符由__getattribute__()方法调用
- 覆盖__getattribute__()方法会组织默认描述符的调用。
- 只有在新式类中__getattribute__()才能使用
-
object.__getattribute__()和
type.__getattribute__()
对__get__()的调用方式不同。 - 数据描述符总是会覆盖实例的字典。
- 非数据描述符有可能会被实例字典覆盖。
super()
方法返回的对象也有一个自定义的__getattribute__()方法来调用描述符。super(B, obj).m()
被调用时,会先在obj.__class__.__mro__
中查找与B
紧邻的基类A
,然后返回A.__dict__['m'].__get__(obj, B)
。如果结果不是描述器,返回m
。如果m
不再实例字典中,则回溯调用object.__getattribute__()
继续查找。
注意,在Python2.2中,如果m
是数据描述符,super(B, obj).m()
只会调用__get__()
。在Python2.3中,除非涉及旧式类,非数据描述符也会被调用。具体的实现细节见Objects/typeobject.c的super_getattro()
方法。
上面的细节显示,object
,type
和super
中描述符的实现是在__getattribute__()方法中。如果继承自object
或者元类提供了类似的功能,类也会继承这个机制。类似的,可以通过重写__getattribute__()](https://docs.python.org/2/方法来关闭描述符调用
描述器例子
下面的代码创建一个类,其对象是数据描述符,会在每次set
或get
时打印一条信息。
In [1]: 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
...:
In [2]: class MyClass(object):
...: x = RevealAccess(10, 'var "x"')
...: y = 5
...:
In [3]: m = MyClass()
In [4]: m.x
Retrieving var "x"
Out[4]: 10
In [5]: m.x = 20
Updating var "x"
In [6]: m.x
Retrieving var "x"
Out[6]: 20
In [7]: m.y
Out[7]: 5
这个协议很简单,并提供让人兴奋的可能性。其中有几个用例非常常见,已经被封装成函数调用。属性,绑定和未绑定的方法,静态方法和类方法都是基于描述符协议。
属性
property()是一种简单构建数据描述符的方法,它在访问属性时触发函数调用。其原型如下:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
下面的代码展示了一了典型应用,定义一个托管属性x
。
class C(object):
def getx(self): return self.__x
def setx(self): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
下面是一个Python版本的property()实现
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
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 set attribute")
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()就很有用了。
比如,电子表格类可能通过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)
函数和方法
Python的面向对象特性是在函数的基础上构建起来的。使用非数据描述符,两者可以无缝联合。
类的字典会将方法存储为函数。在类的定义中,方法使用def lambda来创建,就像声明函数一样。方法与函数的唯一区别是,方法的第一个参数是为对象实例保留的。Python中,实例引用被约定称为self
,实际使用this
或者其他变量名称都是可以的。
为了支持方法调用,函数包括了在属性访问期间绑定__get__()的方法。这意味着所有函数都是非数据描述符,根据从对象调用和从类调用,它们返回绑定或者未绑定的方法,如下是Python的一个实现
class Function(object):
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() int Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
运行解释器来实际看一下(译者使用的是ipython)。
In [1]: class D(object):
...: def f(self, x):
...: return x
...:
In [2]: d = D()
In [3]: D.__dict__['f'] #在内部以函数的形式存储
Out[3]: <function __main__.f>
In [4]: D.f #从类访问得到未绑定的方法
Out[4]: <unbound method D.f>
In [5]: d.f #从对象访问得到一个绑定的方法。
Out[5]: <bound method D.f of <__main__.D object at 0x1101edd10>>
从上面输出表明,绑定和未绑定的方法是两种不同的类型。虽然可以两种类型分开实现,但是在实际的C实现中,是同一个对象通过im_self
这个字段来来进行区分标识的,见 Objects/classobject.c中的PyMethod_Type。
同样的,im_self
字段也影响了调用一个方法对象的效果。如果这个字段被设置(绑定),原始函数会被调用,并且如预期一样将第一个参数设置为实例。如果没有绑定,所有参数将不变地传递到原始函数。instancemethod_call()
的实际实现会稍微复杂一些,因为其中涉及到一些类型检查。
静态方法和类方法
非数据描述符提供了用于将绑定函数的通常模式变化为方法的简单机制。
简单的说,函数有一个__get__()方法,以便在作为属性访问时将它们转换为方法。非数据描述符将obj.f(*args)
转换为f(obj, *args)
。调用kclass.f(*args)
会变成f(*args)
。
下图总结了绑定及两个最有用的变体。
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(kclass, *args) |
静态方法会返回没有更改的底层函数。调用c.f
或者是C.f
等价交换于直接查找object.__getattribute__(c, "f")
或者obj.__getattribute__(C, "f")
。因此,该函数从对象或者类中可以同等访问。
那些不需要self
变量的适合写成静态方法。
比如,统计程序包中可能会使用container
类来封装实验数据。该类提供了用于计算平均数,中位数,以及其他描述性统计指标的方法。然而,可能存在部分功能,概念上相关但是不依赖于数据。例如,erf(x)
是在统计工作中出现的转换例程,但不直接依赖于特定的数据集。类或者是对象可以调用它:s.erf(1.5) --> .9332
或者Sample.erf(1.5) --> .9332
。
既然静态方法会将底层的函数原样返回,那么下面的代码就不会让人奇怪了。
In [1]: class E(object):
def f(x):
print x
f = staticmethod(f)
...:
In [2]: E.f(3)
3
In [3]: E().f(3)
3
使用非数据描述符协议,Python版本的staticmethod()如下:
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
和静态方法不同,类方法在调用函数之前会将类引用预置到参数列表。不管调用者是对象还是类,都是相同的。
In [1]: class E(object):
...: def f(klass, x):
...: return klass.__name__, x
...: f = classmethod(f)
...:
In [2]: print E.f(3)
('E', 3)
In [3]: print E().f(3)
('E', 3)
在函数只需要关心类引用儿不需要关心任何底层数据时,这个特性是很有用的。类方法的一个用途是创建构造函数。在Python2.3中,类方法dict.fromkeys()
会依据一个key列表来创建一个新的字典。等价的Python实现如下。
class Dict(object):
...
def fromkeys(klass, iterable, value=None):
"Emulate dict_fromkeys() in Object/dictobject.c"
d = klass()
for key in iterable:
d[key] = value
return d
fromkeys = classmethod(fromkeys)
现在,可以通过如下方式创建一个字典:
In [1]: dict.fromkeys("abracadabra")
Out[1]: {'a': None, 'b': None, 'c': None, 'd': None, 'r': None}
采用非数据描述符,Python版本的classmethod()实现如下:
class ClassMethod(object):
"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
“本译文仅供个人研习、欣赏语言之用,谢绝任何转载及用于任何商业用途。本译文所涉法律后果均由本人承担。本人同意简书平台在接获有关著作权人的通知后,删除文章。”
网友评论