本篇先介绍后面两种方法。
===============================================================================
特性property
特性协议允许我们把一个特定属性的get和set操作指向我们所提供的函数或方法,使得我们能够插入在属性访问的时候自动运行的代码,拦截属性删除。
通过property内置函数来创建特性并将其分配给类属性,就像方法函数一样。一个特性管理一个单个的、特定的属性;尽管他不能广泛地捕获所有的属性访问,但它允许我们控制访问和赋值操作,并且允许我们自由地把一个属性从简单的数据改变为一个计算,而不会影响已有的代码。
特性和描述符有很大的关系,它们基本上是描述符的一种受限制的形式。
我们可以通过把一个内置函数的结果赋给一个类属性来创建一个特性:
attrbute = property(fget, fset, fdel, doc)这个内置函数的参数都不是必需的,并且如果没有传递参数的话,所有都取默认值None,那么这样的操作是不受支持的。
------------------------------------------------------------------------------------------------------------------------------------------
第一个例子
如下的类使用了一个特性来记录对一个名为name的属性的方法,实际存储的数据名为_name,以便不会和特性搞混了:
class Person: def __init__(self,name): self._name = name def getName(self): print('fetch...') return self._name def setName(self,value): print('change...') self._name = name def delName(self): print('remove...') del self._name name = property(getName,setName,delName,'name property docs') bob = Person('Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name print('-'*20) sue = Person('Sue Jones') print(sue.name) print(Person.name.__doc__)这个特定的特性所做的事情并不多——它只是拦截并跟踪了一个属性。两个实例继承了该特性,就好像它们是附加到其类的另外两个属性一样。然而,捕获了它们的属性访问:
fetch... Bob Smith change... fetch... Robert Smith remove... -------------------- fetch... Sue Jones name property docs就像所有的类属性一样,实例和较低的子类都继承特性。如果我们把例子修改为如下所示:
class Super: def __init__(self,name): self._name = name def getName(self): print('fetch...') return self._name def setName(self,value): print('change...') self._name = value def delName(self): print('remove...') del self._name name = property(getName,setName,delName,'name property docs') class Person(Super): pass bob = Person('Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name print('-'*20) sue = Person('Sue Jones') print(sue.name) print(Person.name.__doc__)输出是相同的,Person子类从Super继承了name特性,并且bob实例从Person获取了它。
------------------------------------------------------------------------------------------------------------------------------------------
计算的属性
上述例子只是简单了跟踪了属性访问,然而,通常特性做的更多——例如,当获取属性的时候,动态地计算属性的值。看下述例子:
class PropSquare: def __init__(self,start): self.value = start def getX(self): return self.value ** 2 def setX(self,value): self.value = value X = property(getX,setX) # 只有get和set,没有del和doc P = PropSquare(3) P = PropSquare(32) print(P.X) # 3**2 P.X = 4 print(P.X) # 4**2 print(Q.X) # 32**2这个例子定义了一个X属性,并且将其当做静态数据一样访问,但实际运行的代码在获取该属性的时候计算了它的值。
------------------------------------------------------------------------------------------------------------------------------------------
使用装饰器编写特性
函数装饰器的语法是:
@decorator def func(args):...Python会自动将其翻译成对等的形式,把函数名重新绑定到可调用的decorator的返回结果上:
def func(args):... func = decorator(func)由于这一映射,证实了内置函数property可以充当一个装饰器,来定义一个函数,当获取一个属性的时候自动运行该函数:
class Person: @property def name(self):...运行的时候,装饰的方法自动传递给property内置函数的第一个参数(即fget)。这其实只是创建一个特性并手动绑定属性名的一种替代语法:
class Person: def name(self):... name = property(name)实际上,property对象也有getter、setter和deleter方法,这些方法指定相应的特性访问器方法并且返回特性自身的一个副本。看下述例子:
class Person: def __init__(self, name): self._name = name @property def name(self): "name property docs" print('fetch...') return self._name @name.setter def name(self,value): print('change...') self._name = value @name.deleter def name(self): print('remove...') del self._name bob = Person('Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name print('-'*20) sue = Person('Sue Jones') print(sue.name) print(Person.name.__doc__)这段代码等同于上文写的第一个例子,在这个例子中,装饰只是编写特性的一种替代方法。输出结果是相同的:
fetch... Bob Smith change... fetch... Robert Smith remove... -------------------- fetch... Sue Jones name property docs和property手动赋值的结果相比,这个例子中,使用装饰器来编写特性只需要3行额外的代码。所以使用装饰器更加方便。
===============================================================================
描述符
描述符提供了拦截属性访问的一种替代方法,它们与特性有很大关系。实际上,特性是描述符的一种——从技术上讲,property内置函数只是创建一个特定类型的描述符的一种简化方式,而这种描述符在属性访问时运行方法函数。
和特性一样,描述符也管理一个单个的、特定的属性。
描述符作为独立的类创建,并且针对想要拦截的属性访问操作提供特定命名的访问器方法——当以相应的方式访问分配给描述符类实例的属性时,描述符类中的获取、设置和删除等方法自动运行:
class Descriptor: "docstring goes here" def __get__(self, instance, owener):... # Return attr value def __set__(self, instance, value):... # Return noting(None) def __del__(self,instance):... # Return noting(None)带有任何这些方法的类都可以看做是描述符,并且当它们的一个实例分配给另一个类的属性的时候,它们的这些方法是特殊的——当访问属性的时候,会自动调用它们。如果这些方法中的任何一个空缺,通常意味着不支持相应类型的访问。然而,和特性不同,省略一个__set__意味着允许这个名字在一个实例中重新定义,因此,要使得一个属性是只读的,我们必须定义__set__来捕获赋值并引发一个异常。
------------------------------------------------------------------------------------------------------------------------------------------
描述符方法参数
__get__、__set__、__del__三种描述符方法都传递了描述符实例(self)以及描述符实例所附加的客户类的实例,__get__访问方法还额外地接收一个owner参数,指定了描述符实例要附加到的类。其中insatance参数要么是访问的属性所属的实例(用于instance.attr),要么当所访问的属性直接属于类的时候是None(用于class.attr)。前者通常针对实例访问计算一个值,如果描述符对象访问是受支持的,后者通常返回self。
例如,在下面的例子中,当获取X.attr的时候,Python自动运行Descriptor类的__get__方法,Subject.attr类属性分配给该方法:
>>> class Descriptor(object): def __get__(self,instance,owner): print(self,instance,owner,sep='\n') >>> class Subject: attr = Descriptor() >>> X = Subject() >>> X.attr <__main__.Descriptor object at 0x032C0F70> <__main__.Subject object at 0x032C0FF0> <class '__main__.Subject'> >>> Subject.attr <__main__.Descriptor object at 0x032C0F70> None <class '__main__.Subject'>注意在第一个属性获取中自动传递到__get__方法中的参数,当获取X.attr的时候,就好像发生了如下转换:
X.attr --> Descriptor.__get__(Subject.attr, X, Subject)当描述符的实例参数为None的时候,该描述符知道将直接访问它。
------------------------------------------------------------------------------------------------------------------------------------------
只读描述符
如下所示,使用特性忽略set方法时,就无法对该属性赋值了。这样可以让属性成为只读的。
>>> class A : def __init__(self,name): self.name = name def getName(self): return self.name name = property(getName) >>> a = A(1) Traceback (most recent call last): File "<pyshell#25>", line 1, in <module> a = A(1) File "<pyshell#23>", line 3, in __init__ self.name = name AttributeError: can't set attribute但是,描述符和特性不同,省略__set__方法不足以让属性称为只读的,因为描述符名称可以赋给一个实例。在下面的例子中,对X.a的属性赋值在实例对象X中存储了a,由此,隐藏了存储在类C中的描述符:
>>> class D: def __get__(*args): print('get') >>> class C: a = D() >>> X = C() >>> X.a get >>> C.a get >>> X.a = 99 >>> X.a 99 >>> list(X.__dict__.keys()) ['a'] >>> Y = C() >>> Y.a get >>> C.a get这就是Python中所有的实例属性赋值工作的方式,并且它允许在它们的实例中类选择性地覆盖类级默认值。
>>> class D: def __get__(*args): print('get') def __set__(*args): raise AttributeError('cannot set') >>> class C: a = D() >>> X = C() >>> X.a get >>> X.a = 99 Traceback (most recent call last): File "<pyshell#35>", line 1, in <module> X.a = 99 File "<pyshell#29>", line 5, in __set__ raise AttributeError('cannot set') AttributeError: cannot set------------------------------------------------------------------------------------------------------------------------------------------
第一个示例
现在让我们来修改前面为特性编写的第一个例子。如下代码定义了一个描述符,来拦截对其客户类中的名为name的一个属性的访问。其方法使用它们的instance参数来访问主体实例中的状态信息:
class Name: "name descriptor docs" def __get__(self,instance,owner): print('fetch...') return instance._name def __set__(self,instance,value): print('change...') instance._name = value def __delete__(self,instance): print('remove...') del instance._name class Person: def __init__(self,name): self._name = name name = Name() bob = Person('Bob Smith') print(bob.name) bob.name = 'Robert Smith' print(bob.name) del bob.name print('-'*20) sue = Person('Sue Jones') print(sue.name) print(Name.__doc__)运行结果如下,和之前的一模一样:
fetch... Bob Smith change... fetch... Robert Smith remove... -------------------- fetch... Sue Jones name descriptor docs当描述符的__get__方法运行的时候,它传递了3个对象来定义其上下文:
... class Super: def __init__(self,name): self._name = name name = Name() class Person(Super): pass ...当然,这个例子也只是追踪了属性的访问。
计算属性
同样的,也可以用来在每次获取属性的时候计算它们的值,如下所示:
class DescSquare: def __init__(self,start): self.value = start def __get__(self,instance,owner): return self.value**2 def __set__(self,instance,value): self.value = value class Client1: X = DescSquare(3) class Client2: X = DescSquare(32) c1 = Client1() c2 = Client2() print(c1.X) c1.X = 4 print(c1.X) print(c2.X)运行结果如下:
9 16 1024------------------------------------------------------------------------------------------------------------------------------------------
在描述符中使用状态信息
在上述两个例子中,你可能会发现它们从不同的地方获取信息——第一个例子使用存储在客户实例中的数据,第二个例子使用附加到描述符对象本身的数据。实际上,描述符可以使用实例状态和描述符状态,或者二者的任何组合:
【1】描述符状态用来管理内部用于描述符工作的数据
【2】实例状态记录了和客户类相关的信息,以及可能由客户类创建的信息
------------------------------------------------------------------------------------------------------------------------------------------
特性和描述符是如何相关的
特性和描述符有很强的相关性——property内置函数只是创建描述符的一种方便方式。既然已经知道了二者是如何工作的,我们可以使用如下的一个描述符类来模拟property内置函数:
class Property: 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,instance,instancetype=None): if instance is None: return self if self.fget is None: raise AttributeError("can't get attribute") return self.fget(instance) def __set__(self,instance,value): if self.fset is None: raise AttributeError("can't set attribute") self.fset(instance,value) def __delete__(self,instance): if self.fdel is None: raise AttributeError("can't delete attribute") self.fdel(instance) class Person: def getName(self):... def setName(self,value):... name = Property(getName,setName)这个Property类捕获了带有描述符协议的属性访问,并且把请求定位到创建类的时候在描述符状态中传入和保存的函数或方法。