Python——管理属性(1)


管理属性
这里将展开介绍前面提到的【属性拦截】技术。包括以下内容:

【1】__getattr__和__setattr__方法,把未定义的属性获取和所有的属性赋值指向通用的处理器方法
【2】__getattribute__方法,把所有属性获取都指向一个泛型处理器
【3】property内置函数,把特定属性访问定位到get和set处理器函数,也叫做特性(property)
【4】描述符协议,把特定属性访问定位到具有任意get和set处理器方法的类的实例

最后两种方法适用于特定属性,而前面两种方法足够通用。

本篇先介绍后面两种方法。

===============================================================================

特性property
特性协议允许我们把一个特定属性的get和set操作指向我们所提供的函数或方法,使得我们能够插入在属性访问的时候自动运行的代码,拦截属性删除。

通过property内置函数来创建特性并将其分配给类属性,就像方法函数一样。一个特性管理一个单个的、特定的属性;尽管他不能广泛地捕获所有的属性访问,但它允许我们控制访问和赋值操作,并且允许我们自由地把一个属性从简单的数据改变为一个计算,而不会影响已有的代码。

特性和描述符有很大的关系,它们基本上是描述符的一种受限制的形式。

我们可以通过把一个内置函数的结果赋给一个类属性来创建一个特性:

 

attrbute = property(fget, fset, fdel, doc)
这个内置函数的参数都不是必需的,并且如果没有传递参数的话,所有都取默认值None,那么这样的操作是不受支持的。
当使用它们的时候,我们向fget传递一个函数来拦截属性访问,给fset传递一个函数进行赋值,并且给fdel传递一个函数进行属性删除;doc参数接收该属性的一个文档字符串,如果想要的话。fget返回计算过的属性值,并且fset和fdel不会返回数据,返回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>


>>> Subject.attr
<__main__.Descriptor object at 0x032C0F70>
None
注意在第一个属性获取中自动传递到__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 "", line 1, in 
    a = A(1)
  File "", 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中所有的实例属性赋值工作的方式,并且它允许在它们的实例中类选择性地覆盖类级默认值。
要让基于描述符的属性成为可读的,捕获描述符类中的赋值并引发一个异常来阻止属性赋值——当要赋值的属性是一个描述符的时候,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 "", line 1, in 
    X.a = 99
  File "", 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个对象来定义其上下文:
【1】self是Name的实例
【2】instance是Person类实例
【3】owner是Person类实例
还和特性示例中类似,描述符实例是一个类属性,并且因此由客户类和任何子类的所有实例继承。如果我们把示例中的Person类修改为如下的样子,脚本的输出是相同的:
...
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类捕获了带有描述符协议的属性访问,并且把请求定位到创建类的时候在描述符状态中传入和保存的函数或方法。

 

评论关闭