Descriptor

引用一个对象的属性是是直接访问属性,没有什么复杂的。直接获取和设置属性影响到名字空间里的值。有时当访问这些值时,还有些事要处理的。

1)从一个复杂的出处获取数据,诸如数据库或配置文件
2)把一个简单的值转换成一个复杂的对象或数据结构
3)为对象定制值
4)在保存到数据库中,把值转变成保存前的格式。

在 有些编程语言里,这些行为是通过创建一些附加的实例方法来访问需要的属性。while functional, 这种方法会带来一些问题的。对于starter,这些行为相比于他们附属的实例的某些方面,对存储在属性里的数据结构关联度更紧。通过要求对象配备访问这 个数据的方法,包含这个行为的对象必须在实例方法中提供必要的代码。

另一个比较大的问题是当本来是简单使用的属性突然需要更多的行为特征怎么办?从一个简单的属性改变成方法,所有属性的引用都要发生改变。为了避免这个问题,这些语言的编程者,采纳了一些标准的实践来创建属性访问的方法以便底层实现的改变不影响已经存在的代码。

为 了一个属性访问的改变,而去查看你的很多代码,这永远不是招人喜欢的事。因此python提供了一个方法来解决这个问题。宁可要求对象负责属性的访问,也 不要属性自己来提供这种行为。Descriptor就是这样特殊类型的对象,当依附于一个类,就能介入什么时候访问属性,提供任何更多的行为。

>>> import datetime
>>> class CurrentDate(object):
...     def __get__(self, instance, owner):
...         return datetime.date.today()
...     def __set__(self, instance, value):
...         raise NotImplementedError("Can't change the current date.")
...
>>> class Example(object):
...     date = CurrentDate()
...
>>> e = Example()
>>> e.date
datetime.date(2009, 3, 27)
>>> e.date = datetime.date.today()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __set__
NotImplementedError: Can't change the current date.

创建一个Descriptor,就像创建一个新式的类(从object继承)一样简单。最少指定下面要说的方法。descriptor类还能包括其他的属性或方法来执行负责的任务。下列的方法组成了激活这种行为的约定。

__get__(self, instance, owner)

当提取一个属性的值时,value = obj.attr,这个方法就调用了。允许descriptor在返回值之前作一些额外的事。除了self代表descriptor对象自身,这个getter方法接受2个参数。

1)instance

这是要引用的包含属性的实例对象,如果这个属性是类属性而不是实例属性的话,这个参数就是None

2)owner

要分配descriptor的类,它总是一个类对象。

instance参数是用来决定descriptor是否从一个对象或它的类中被访问。如果instance是None,这个属性就是从类中被访问,而不是从实例访问,如果descriptor没有按本应该的方式来访问,就抛出异常。

同时,通过定义这个方法,你用descriptor来负责提取和返回一个值给请求值的代码。失败的话就强制python返回一个默认的返回值None。

注意,默认情况下,当声明一个属性时,descriptor不知道他们被给定的是什么名称。django model提供了一个方法来得到名称,这会在第3章有描述,但是除了这个,descriptor只知道它们的值,而不是它们的名称。

__set__(self, instance, value)

当设置一个值给一个descriptor(obj.attr = value),这个方法就被调用以便能进行更多的处理。像__get__一样,这个方法接受除了标准的self,还有2个参数。

1)instance -- 实例对象,包含要引用的属性,永远不能是None。

2)value -- 被分配的值。

应 该注意descriptor的__set__方法只在属性分配到一个对象时才被调用,分配给一个类的属性(此时descriptor开始分配) 时,__set__是不被调用的。这个行为通过设计,禁止descriptor完全控制它的访问。外部的代码还可以分配一个值给第一次赋值的类,来替换 descriptor。

同时注意,从__set__返回的值是不相关的,这个方法只是负责正确的保存被配置的值。

跟踪实例数据

因为descriptor能简化属性的存取,你需要注意什么时候给附属的对象设置值。你不能只是简单的用settattr来设置对象的值。这样作的后果,再次调用descriptor会导致无穷的循环。

python 提供了另外一种方法访问一个对象的名字空间,__dict__属性。python所有的对象上都有__dict__,它是一个字典,存放对象名字空间所有 值。直接访问这个字典,就可以绕开不用python标准处理属性的方式,包括descriptor。使用这个技巧,descriptor可以设置一个对象 的值,不用触发它自己。考虑下面的例子。

>>> class Descriptor(object):
...     def __init__(self, name):
...         self.name = name
...     def __get__(self, instance, owner):
...         return instance.__dict__[self.name]
...     def __set__(self, instance, value):
...         instance.__dict__[self.name] = value
...
>>> class TestObject(object):
...     attr = Descriptor('attr')
...
>>> test = TestObject()
>>> test.attr = 6
>>> test.attr
6

不足的是,这种技巧要求显式把属性名称指明给descriptor。你能用一些metaclass技巧来处理,Django的model系统展示了这种技法(第3章有讨论)。