上一次,我们简单介绍了一下Descriptor,但是由于Descriptor默认是类属性,如果我们需要更进一步的控制,就是针对每个属性都能够使用__get__, __set__进行访问的话,就需要property来协助了。
Property Introduction
property就是一个特殊的Descriptor,当我们对这个属性进行访问时,将会自动转化为对实例属性的访问(如果调用时提供了实例)。先来看一个例子:
class Foo(object): def set_x(self, val): print '%s %d' % ('set_x', val) self._x = val def get_x(self): print '%s' % ('get_x') return self._x x = property(get_x, set_x) f = Foo() f.x = 1 print f.x
输出结果将是
set_x 1 get_x 1
property是一个内建函数,该函数的原型是
property([fget[, fset[, fdel[, doc]]]])
我们只提供了fget和fset,因为这是最常用的。
我们可以使用decorator来创建只读的属性,以下是python manual中的一个例子
class Parrot(object): def __init__(self): self._voltage = 100000 @property def voltage(self): """Get the current voltage.""" return self._voltage
可以提供一个名字为voltage的函数,用decorator装饰后,就变成只读属性了,这样也就不需要显式地使用 voltage = property(get_voltage)来创建property了。
回想一下,我们如果创建一个属性为 a 的非数据描述符的时候,我们还是可以对 a 进行赋值,这是因为,python把值放在了实例的__dict__中。但是这里当我们试图修改voltage时,将会出现如下错误:
>>> p.voltage = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute
Implement property with Python
显然,property返回的描述符是一个数据描述符,并且当我们只提供了get函数的时候,__set__将会抛出AttributeError异常。我们用python来简单实现一下:
class PyProperty(object): def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc: self.__class__.__doc__ = doc elif fget.__doc__: self.__class__.__doc__ = fget.__doc__ def __set__(self, instance, val): if self.fset: self.fset(instance, val) else: raise AttributeError("Can't set attribute") def __get__(self, instance, owner): if owner: if self.fget: return self.fget(instance) else: raise AttributeError("unreadable attribute") else: return self def __delete__(self, instance): if self.fdel: self.fdel(instance) else: raise AttributeError("Can't delete attribute") class Foo3(object): def get_x(self): print 'get_x' return self._x def set_x(self, v): print 'set_x' self._x = v def del_x(self): print 'del_x' x = property(get_x, set_x, del_x) f = Foo3() f.x = 1 print f.x del f.x print Foo3.x.__doc__
这里,输出将是:
set_x get_x 1 del_x Foo3.x
这里输出了正确的结果。
Class scope name space
每次我们要添加一个property的时候,就必须提供getter, setter等函数,但是如果直接在class中存放,很快类中函数将会遍布这些小函数,有一个解决方是利用函数来做name space,把getter, setter等函数都放入一个函数中,代码如下:
class Foo4(object): def x(): # name must be same as property's parameters def fget(self): print 'get_x' return self._x def fset(self, v): print 'set_x' self._x = v return locals() x = property(**x()) f = Foo4() f.x = 1 print f.x
这里,我们使用了一个函数x在其中定义嵌套函数fget, fset。之后我们利用locals函数返回 x 中局部名称,也就是fget, fset。locals返回一个dictionary,所以我们使用** unpacking operator,将dictionary unpacking后,传递给property。这里getter, setter的函数名字很重要,必须是fget和fset,也就是必须同property定义时的named parameters的名字一样,因为我们传递的是named arguments。
Other Note
细心的人可能已经注意到,我们在set_x, get_x等函数中,都是用了_x,而不是x,原因是因为如果使用x,那么我们就会陷入到递归调用的境地。当我们从外界请求属性x,此时调用了get_x,而get_x又再一次请求x,这样就递归了,所以我们通常取一个其他名字来代替属性的名字。
references
[1] 《Core python programming》