描述符是实现描述符协议方法的 Python 对象, 当将其作为其他对象的属性进行访问时, 该描述符使您能够创建具有特殊行为的对象.
通常, 描述符是具有 "绑定行为" 的对象属性, 其属性访问已被描述符协议中的方法所覆盖. 这些方法是__get __(),__set __()和__delete __(). 如果为对象定义了这些方法中的任何一种, 则称其为描述符. 属性访问的默认行为是从对象的字典中获取, 设置或删除属性. 例如, a.x 具有一个查找链, 查找链从 a .__ dict __ ['x']开始, 然后键入 (a).__ dict __ ['x'], 并继续遍历类型(a) 的基类 (不包括元类). 如果查找到的值是定义描述符方法之一的对象, 则 Python 可能会覆盖默认行为并改为调用描述符方法. 优先链在何处发生取决于定义了哪些描述符方法. 描述符是功能强大的通用协议. 它们是属性, 方法, 静态方法, 类方法和 super() 背后的机制. 在 Python 本身中使用它们来实现 2.2 版中引入的新样式类.
- descr.__get__(self, obj, type=None) -> value
- descr.__set__(self, obj, value) -> None
- descr.__delete__(self, obj) -> None
定义这些方法中的任何一个, 对象被视为描述符, 并且在被视为属性时可以覆盖默认行为.
如果对象定义了__set __()或__delete __(), 则将其视为数据描述符. 仅定义__get __()的描述符称为非数据描述符(它们通常用于方法, 但也可以用于其他用途). 数据和非数据描述符在实例字典中替代计算方式方面有所不同. 如果实例的字典中的属性名称与数据描述符的名称相同, 则以数据描述符为准. 如果实例的字典中具有与非数据描述符同名的属性, 则该字典属性优先. 我们来看一下例子:
- class lazy(object):
- def __init__(self, func):
- self.func = func
- def __get__(self, instance, owner):
- val = self.func(instance)
- setattr(instance, self.func.__name__, val)
- return val
- class Circle(object):
- def __init__(self, radius):
- self.radius = radius
- @lazy
- def area(self):
- print('evalute')
- return 3.14 * self.radius ** 2
- def __getattr__(self, item):
- return 1
- c = Circle(4)
- print(c.area)
- print(c.area)
输出结果是
evalute 50.24 50.24
我们定义了一个描述符的类 lazy, 它只实现了__get__方法, 是一个非数据的描述符, 我们用它定义了类 Circle 中的 area 方法, 所以 area 方法成为了一个描述符的对象, 可以看到, 在第一次调用 c.area 的时候, 执行了 area 的方法, 打印了 "evalute", 在第二次的时间就直接输出了结果, 没有指向 area 的方法, 这是为什么呢?
那么重点来了, 可以看到在 lazy 定义的__get__方法中, 执行了被描述对象的方法, 也就是这里的 area 函数, 获取到结果之后, 给当前的 instance 设置了一个同名的属性, 并且设值为结果, 这样下次在调用的时间, 因为这是一个非数据的描述符, 看上面的黑体字, 实例的字典中的属性名称与数据描述符的名称相同, 则以数据描述符为准. 所以会取你刚刚设置的属性的值, 不会再去取描述符的值. 我们再来看看数据描述符的一个例子:
- class lazy(object):
- def __init__(self, func):
- self.func = func
- def __get__(self, instance, owner):
- val = self.func(instance)
- setattr(instance, self.func.__name__, val)
- return val
- def __set__(self, instance, value):
- pass
- class Circle(object):
- def __init__(self, radius):
- self.radius = radius
- @lazy
- def area(self):
- print('evalute')
- return 3.14 * self.radius ** 2
- def __getattr__(self, item):
- return 1
- c = Circle(4)
- print(c.area)
- print(c.area)
我们看一下输出的结果:
- evalute
- 50.24
- evalute
- 50.24
同样的定义, 只是在描述符中添加了__set__方法, 就会执行调用描述符定义的属性, 和非描述符的调用方式天壤之别. 这就是这个高级特性的特别之处. 我们可以使用非数据描述符做惰性加载, 只计算一次, 下次直接取值, 我在工作中也是这样干的.
知其然, 知其所以然, 我们来看一下是为什么:
根据官方的解释, 描述符可以通过其方法名称直接调用. 例如, d .__ get __(obj). 另外, 更常见的是在属性访问时自动调用描述符. 例如, obj.d 在 obj 的字典中查找 d. 如果 d 定义了方法__get __(), 则根据下面列出的优先级规则调用 d .__ get __(obj). 调用的细节取决于 obj 是对象还是类.
对于对象, 机制位于 object .__ getattribute __()中, 它将 b.x 转换为 type(b).__ dict __ ['x'] .__ get __(b,type(b)). 该实现通过优先级链进行工作, 该优先级链赋予数据描述符优先于实例变量的优先级, 实例变量优先于非数据描述符的优先级, 并为__getattr __()分配最低优先级. 完整的 C 实现可在 Objects / object.c 中的 PyObject_GenericGetAttr()中找到.
对于类, 机制的类型为.__ getattribute __(), 它将 B.x 转换为 B .__ dict __ ['x'] .__ get __(无, 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 __()防止自动描述符调用
object .__ getattribute __()和 type .__ getattribute __()对__get __()进行不同的调用.
数据描述符始终会覆盖实例字典. 非数据描述符可以被实例字典覆盖.
具体的可以查看 Python 的 c 源码.
以上就是今天要和大家一起学习的内容.
代码地址
https://github.com/oldman1991/testdemo/blob/master/0028_python_descriptor.py
来源: http://www.bubuko.com/infodetail-3365648.html