Python里的描述器 Descriptor

起因

在公众号里读到了这篇文章《这个 Python 知识点,90% 的人都得挂~》,一直读到第三点 基于描述符如何实现property 的时候懵了,于是去翻文档重新学习。(公众号的文章里叫描述符,翻译的文档叫描述器,以下统一称Descriptor为描述器)

学!

什么是描述器

descriptor 就是任何一个定义了 __get__()__set__()__delete__() 的对象。

可选地,描述器可以具有 __set_name__() 方法。这仅在描述器需要知道创建它的类或分配给它的类变量名称时使用。(即使该类不是描述器,只要此方法存在就会调用。)

描述器的四个方法

set_name

这个函数甚至在公众号的文章里没有被介绍(不然最后那部分的代码可以简洁很多),它是3.6版本的新功能,一般定义如下:

	def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

其中,self 代表着描述器本身,owner 代表了创建它的类, name 代表了分配给它的类变量名。
举个例子:

class Component:

    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    ...

假设这里的String是一个描述器,那么上述的self owner name就分别是name(String的一个实例) Component类 字符串‘name’ 。那么有可能有人就要问了,到底有啥用呢?这里就要提到描述器的一种流行用法,就是托管对实例数据的访问,描述器被分配给类字典中的公开属性,而实际数据作为私有属性存储在实例字典中。

在上面一个代码块里,我们实现了描述器被分配给类字典中的公开属性这第一步,那么第二步我们先通过这个函数得到了对应的私有属性名,即self.private_name,数据怎么储存就看下面的函数啦。

set

先给出标准的定义:

	def __set__(self, obj, value):
        setattr(obj, self.private_name, value)

这里obj就是绑定了描述符的实例,通过setattr直接把实际数据储存在实例的字典里。

get

	def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        return value

如何拿到对应的数据,这里的objtypeobj这个实例的类型。
如果不是通过实例(a.x)而是通过类来访问(A.x),会直接调用desc.__get__(None, A),因此有些会加个判断:

	def __get__(self, obj, objtype=None):
	    if obj is None:
	        return self
        value = getattr(obj, self.private_name)
        return value

补充

可以看到上面函数里面,像__set_name__的参数owner__get__的参数objtype你怎么没有用到呀。因为上面介绍用的例子都太简单了,甚至可以 说是没啥用。

想这个用描述器来实现简单ORM框架的例子,就通过owner拿到了对应类的类属性。

属性的访问

如果一个对象定义了 __set__()__delete__(),则它会被视为数据描述器。 仅定义了 __get__() 的描述器称为非数据描述器(它们经常被用于方法,但也可以有其他用途)。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。对于实例来说,a.x 的查找顺序会从 a.__dict__[‘x’] 开始,然后是 type(a).__dict__[‘x’],接下来依次查找 type(a) 的方法解析顺序(MRO)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重写默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

点运算符的查找逻辑在 object.__getattribute__() 中。这里是一个等价的纯 Python 实现:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

可以看到数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__() (如果存在的话)。
如果是描述器的话,会直接调用描述器的__get__去拿数据。

Property的实现(解惑)

定义:

class Property:
    "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
        if doc is None and fget is not None:
            doc = fget.__doc__
        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__)

调用:

class Student:
    def __init__(self, name):
        self.name = name

    # 其实只有这里改变
    @Property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

我们看到第一个装饰器,等价于math = Property(fget=math, fset=None, fdel=None, doc=None),于是math变成了一个描述器的实例,对于整个Student类来说,类变量math绑定了一个描述器,正好符合描述器的使用场景。

接着看第二个装饰器,等价于math = math.setter(math),等价于math = Property(fget=math, fset=math, None, None),看着可能有点晕,参数里的第一个math是上面装饰器装饰的函数,储存在第一个实例的self.get里面,作为参数传入到第二个实例的构造函数里面,第二个math就是第二个装饰器装饰的函数啦。

想通了应该还是比较好理解的,总结一下就是把类变量math绑定到了一个描述器上,第一个函数作为fget传入,第二个函数作为fset传入。但我们访问或改变Student实例的math属性时,就会调用描述器里的__get____set__函数,而它们又会分别调用我们刚刚用装饰器装饰的两个函数,感觉是绕了一圈呀。

应用

描述器的使用贯穿了整个语言。就是它让函数变成绑定方法。常见工具诸如 classmethod()staticmethod()property()functools.cached_property() 都作为描述器实现。
纯 Python 版本的实现在文档里都有,但我觉得最秀的还是验证器和ORM啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值