UP | HOME

Python 描述器简述

目录

1 前言

前端时间在阅读 python-patterns 的源码的时候, 其中一个模式的实现使用到了 描述器.

当时看到诸如 __get__ 的方法的时候, 还以为很简单, 毕竟之前已经整理学习过一次 Python 魔法方法.

然而, 事实就是, 看不懂 !!!

经历一番研究后, 对 描述器 的理解总算有了一些眉目, 剩下的更多的是需要实践操作中进行理解。

当然, 我不觉得目前我需要在什么程序中用到描述器……

为了不至于忘了描述器是怎么一回事, 我觉得有必要用这篇博客记录下来。

2 描述器

和描述器密切相关的魔法方法有四个, 其中: __get__, __set____delete__ 是描述器协议的组成部分。

__getattribute__ 影响 描述器 的调用。

理解 描述器 的关键点在于 __getattribute__, 如果你不清楚 __getattribute__ 的作用, 那么请记住:

__getattribute__Python 在获取 对象属性 时调用的方法, 原型为: __getattribute__(self, name).

其中 name 是要获取的属性的名称, 而该方法的 返回值 就是获取的属性的值。

描述器官方教程 中有这样一段代码, 大致告诉了我们 描述器 是怎么一回事:

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

通过这段代码可以看到, 当我们尝试去获取属性 key 的值的时候, __getattribute__ 会尝试判读获取到的值 v 是否存在属性 __get__.

如果存在, 就将返回 v.__get__(None, self) 的值, 否则返回 v.

这一段很关键, 告诉了我们非常重要的一件事: 描述器 不是在获取 描述器属性 时调用的, 而是在 获取描述器 时调用的。

先看代码:

class Descriptor(object):
    def __get__(self, obj, obj_type):
        print('call __get__')
        return 0


class Test(object):
    descriptor = Descriptor()

Test.descriptor

上述代码的输出为:

call __get__

可以看到, 获取 Testdescriptor 属性时, Python 发现 descriptor 是一个 描述器, 于是调用了这个描述器的 __get__ 方法。

当然, 上述代码是对于 来说的, 描述器的调用在 实例 上存在一定的区别。

在说的这个区别前, 需要了解的一个概念是 描述器 的类型:

  • 资料描述器: 同时定义了 __get____set__ 方法的描述器是资料描述器
  • 非资料描述器: 仅定义了 __get__ 方法的描述器是非资料描述器

这两种描述器的区别主要在于 调用优先级 的区别。

假设存在 obj.x 的调用, 其中 x 是一个 描述器:

  • 如果 obj 是一个 实例, 那么调用顺序依次为: 资料描述器 -> 实例字典 -> 非资料描述器.

    如果 obj 的实例属性中存在和 x 同名的 属性, 同时 x 是一个 非资料描述器, 那么 obj.x 获取的会是 实例属性 的值。 反之, 如果 x 是一个 资料描述器, 那么获取的会是这个 资料描述器 的值。

  • 如果 obj 是一个 , 那么 obj.x 的调用等价于之前的那段代码:

    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__ 会影响描述器的正常调用。

3 描述器的使用

描述器的方法原型为:

descr.__get__(self, obj, obj_type) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

其中, obj 代表的是 实例, 如果描述器的拥有者是 , 那么 obj 就为 None.

obj_type 代表的就是 , 而 value 就是你要设置的值。

对于描述器的使用场景, 我编写程序还没遇到过需要使用描述器的地方, 只能拿教程里的一个例子来说明了:

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    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, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError, "unreadable attribute"
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError, "can't set attribute"
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError, "can't delete attribute"
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

上述代码是内置函数 property 的一个等价实现, 至于 property 的作用我想不需要我多说了。

简单的理解代码:

  • 构造函数, 当使用 property 装饰一个方法时会被调用, 装饰的方法会作为 fget 传入, 然后被装饰的方法变成了一个资料装饰器 - Property 的实例。
  • 当调用 obj.func 时, 这个 func 是一个 资料装饰器, 因此会调用 Property__get__ 方法。

emmm, 剩下的代码就很简单了。

最后, 描述器很强大, 使用请谨慎.

4 参考链接

版权声明:本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可