聊聊Python中的描述符

描述符是实现描述符协议方法的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
更多问题欢迎关注微信公众号