描述器

对于Python的描述器的作用,我们可以先记住一句话:描述器是描述类的属性的。

描述器的魔术方法

先思考下面程序的执行流程:

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')


class B:
    x = A() # 定义(描述)类的属性
    def __init__(self):
        print('B.init')

print(B.x.a1) # 会输出什么?

b = B()
print(b.x.a1) # 会输出什么?

上例中,x作为B的类属性,但x并非是一个简单数据类型,而是A类的实例化对象。因此,访问属性x时,必然会实例化A。

因此,分别输出为:

print(B.x.a1) # 会输出
A.init
a1

---------------

b = B()
# 输出
A.init
B.init

print(b.x.a1) # 输出
a1

上例的代码,已经可以简单认为B类的属性x,被A类的实例来"描述"了。但Python中的描述器,有更为精确的定义,它要求必须实现某些魔术方法。

__get__方法

__get__(self, instance, owner)

如果对上述A类,做一些改造。比如在A类中实现__get__方法,看会发生什么事情。

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self, instance, owner): # A类增加了__get__方法
        print("Class A.__get__{} {} {}".format(self,instance,owner))

class B:
    x = A()
    def __init__(self):
        print('B.init')


print(B.x) # 会输出什么?
# 输出:
# A.init
# Class A.__get__<__main__.A object at 0x000001EDE1170A20> None <class '__main__.B'>
# None

b = B()
print(b.x) # 会输出什么?为什么?
# 输出:
# A.init
# B.init
# Class A.__get__<__main__.A object at 0x000001DDDF3A0A20> <__main__.B object at 0x000001DDDF3A0EF0> <class '__main__.B'>
# None

print(b.x.a1)
# 异常AttributeError: 'NoneType' object has no attribute 'a1'
# b.x竟然是None?

解释:因为定义了__get__方法,A类就是一个描述器。

当对B类或者B类的实例的x属性读取时,变成对A类的实例的访问时,就会调用此方法。此方法的返回值,会作为A的实例化对象的返回值。

简单而言,B类的x属性,使用了一个A类去描述(定义)它。如果对x属性访问时,就会调用A类的__get__方法(描述方法),其返回值会返回给x属性。

三个参数的解析:

参数


说明

self

<__main__.A object at 0x000001DDDF3A0A20>

指当前实例,调用者 A(). 描述器实例自身

instance

<__main__.B object at 0x000001DDDF3A0EF0>

指代owner的实例 B(),描述器拥有者的实例对象

owner

<class '__main__.B'>

指属性所属的类 B,谁拥有这个描述器

正常情况下,__get__魔术方法需要返回self,也就是A的实例,才能正常返回a1属性。

def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return self

此时,我们可以总结描述器的定义了。

描述器的定义

描述器定义:

Python中,一个类实现了__get__,__set__,__delete__ ,三个方法中任何一个方法,就是描述器。

如果仅实现了__get__,就是非数据描述器non-data descriptor;

同时实现了__get__,__set__就是数据描述器 data descriptor;

因此,上述的例子,属于非数据描述器。

描述器一般都会实现__get__方法,因为此方法的返回值就是作为被描述的类的属性的值。

因此,一个类的属性,完全可以使用另一个类的实例来描述它。由于类的实例完全可以自由定义,对于某个类属性,可以借助类,来支撑起更为强大的功能。

如果一个类的类属性设置为描述器,那么它被称为owner属主。比如上例中的B,它拥有了描述器A。

属性的访问顺序

为上例中的B类增加实例属性x

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return self
    

class B:
    x = A()  # 一定会输出A.init,因为定义必须有值,而该值必须是A的实例化。
    def __init__(self):
        print('B init')
        self.x = 'b.x' # 增加实例属性x

print(B.x) # 输出:
# A.__get__<__main__.A object at 0x0000019BD760B400> None <class '__main__.B'>
# <__main__.A object at 0x0000019BD760B400>  x就是A的实例self


print(B.x.a1) # 输出:
#A.__get__<__main__.A object at 0x000002929C0BB400> None <class '__main__.B'>
#a1

b = B()
print(b.x)
# A init
# B init
# b.x  访问了实例属性

print(b.x.a1) # AttributeError: 'str' object has no attribute 'a1'

b.x访问到了实例的属性,而不是描述器。

也就是说,对于非数据描述器,属性的访问顺序是符合一般预期的。

总结:

非数据描述器,只对类属性产生作用。当访问类属性时,将调用描述器的__get__方法

__set__方法

__set__(self,instance,value)

这里的instance是什么? value是什么?

继续修改代码,为类A增加__set__方法。

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return self

    def __set__(self,instance,value):
        print('A.__set__ {} {} {}'.format(self,instance,value))
        self.data = value

class B:
    x = A()  # 一定会输出A.init
    def __init__(self):
        print('B init')
        self.x = 'b.x' # 增加实例属性x

#print(B.x)
#print(B.x.a1)
# 输出
# A.init
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# <__main__.A object at 0x00000276E5920B38>
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# a1

b = B()
# 输出:
# A.init
# B init
# A.__set__ <__main__.A object at 0x000002CE77C40B38> <__main__.B object at 0x000002CE77C40F60> b.x   b实例属性的值传递到A上了。 看样子,调用了__set__

print(b.x) # 还是访问b实例属性吗?还是说变成了被描述器描述的属性?
print(b.x.a1)
# 输出:
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# <__main__.A object at 0x0000023250DE0B38>
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# a1   访问b.x.a1,返回了描述器的数据

如果当b进行属性赋值时(self.x = ‘b.x’),此时调用了set了。注意,此属性必须是被描述器描述过的类属性。

此时instance为b,values为b.x 。

一般都会使用实例来调用,而不是类来调用如B.x,因为B.x就不会调用__set__方法。

当使用了__set__方法后,属性的访问顺序发生了变化:

数据描述器 -->
实例的__dict__ -->
非数据描述器

数据描述器属性访问优先于实例的_dict_

实例的__dict__优先于非数据描述器。

__delete__方法在删除相关属性时触发,具有同样的效果,有了这个方法,就是数据描述器。

本质

观察数据描述器和非数据描述器时,属性字典的变化:

b.__dict__和 B.__dict__

屏蔽了__set__方法结果如下:(非数据描述器)

class A:
    n = 'A.x'

    def __get__(self, instance, owner):
        return self

    # def __set__(self, instance, value):
    #     print('Call A __set__')

class B:
    x = A()

    def __init__(self):
        self.x = 'b.x'
        self.y = 'b.y'

print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001C9262370F0>, '__init__': <function B.__init__ at 0x000001C9262D7AE8>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}

print(B().__dict__)
# {'x': 'b.x', 'y': 'b.y'} # 实例字典符合一般预期

不屏蔽__set__方法结果如下:(数据描述器)

print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001D02F1570F0>, '__init__': <function B.__init__ at 0x000001D02F1F7B70>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}


print(B().__dict__)
# Call A __set__
# {'y': 'b.y'} # x属性不见了

因此,是数据描述器时,把实例属性从属性字典中去除了,此时当然先访问数据描述器了。

为什么b.x不存在b的字典中?按以前的理解,应该会添加一个key value。但由于它被描述了,行为就不同了。

注意,因为b.y不是描述器,则它的行为还是如常表现,加入到字典中。

而B的dict也不变,因为实例的赋值不会影响类属性。

因此,如果类的属性是一个描述器,此时实例属性赋值的行为就改变了。但是对于用户而言,并不需要关心它是否为描述器。因此,描述器的行为就必须表现得和普通的属性赋值一样。

因此,需要做以下处理:

def __set__(self,instance,value):
         print('A.__set__ {} {} {}'.format(self,instance,value))
         instance.__dict__['x'] = value

此时,描述器进行赋值时,对类属性的读或者写会调用描述器的__get__或者__set__方法。

而instance为b,此时在set方法中把‘x’的值手动添加一个kv到instance的dict中。如此再访问b.x的时候,注意是访问描述器,然后到__get__方法中。

因为b.x一定会访问描述器,而不是b.__dict__。此时拦截了访问字典的路径了。

如果b.x需要返回’b.x’,可以使用直接的方式:

def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return instance.__dict__['x']

总结

数据描述器,针对类属性和实例属性都产生作用。当访问或者修改此属性时,将调用相应的__get__或者__set__方法

我们在《浅谈Python中的反射》这篇文章中知道,当定义了魔术方法__setattr__时,当对属性进行设置时,会阻止设置实例属性加入实例字典的行为,而调用此方法。

而魔术方法__set__中又得知,当对属性进行设置时,会调用此方法。那么,这两个方法的访问优先级是如何的?

看下面的例子:

class A:
    n = 'A.x'
    def __init__(self,name):
        self.name = name

    def __get__(self, instance, owner):
        print('Call A __get__')
        return self

    def __set__(self, instance, value):
        print('Call A __set__')



class B:
    x = A('x')

    def __init__(self):
        self.x = 'b.x'

    def __setattr__(self, key, value):
        print('Call B __setattr__')

b = B()
b.x = 'b.x1'
print(b.x)

# 输出
# Call B __setattr__
# Call B __setattr__
# Call A __get__
# <__main__.A object at 0x0000020BC64E0A20>

以上结果可知,当设置实例属性时,先调用实例本身的__setattr__方法,当读取属性时调用了描述器的__get__方法

因此,我们可以更为准确得出实例属性访问顺序:

实例调用__getattribute__ --> 
__setattr__ 拦截 -->
(数据描述器) -->
instance.__dict__ --> 
(非数据描述器) -->
instance.__class__.__dict__ --> 
继承的祖先类(直到object).__dict__ --> 
找不到 --> 
调用__getattr__

Python中描述器的应用

描述器在Python中应用非常广泛。

Python的方法,普通的实例方法,包括staticmethodclassmethod,都实现为非数据描述器。因此,实例可以重新定义和覆盖方法,表现为正常的字典属性的访问和修改 。这允许单个实例获取与同一类的其他实例不同的行为。

property()函数实现为一个数据描述器,因此,实例不能覆盖属性的行为。

class A:
    @classmethod  # 非数据描述器
    def foo(cls):
        pass

    @staticmethod  # 非数据描述器
    def bar():
        pass

    @property  # 数据描述器
    def z(self):
        return 5

    def getfoo(self): # 非数据描述器
        return self.foo

    def __init__(self): # 非数据描述器
        self.foo = 100
        self.bar = 200
        #self.z = 300 # AttributeError: can't set attribute  无法覆盖数据描述器的定义


a = A()
print(a.__dict__)  # {'foo': 100, 'bar': 200} 看不到z属性
print(A.__dict__) # {'__module__': '__main__', 'foo': <classmethod object at 0x0000017674C6B7B8>, 'bar': <staticmethod object at 0x0000017674C6B7F0>, 'z': <property object at 0x0000017674BCD598>, 'getfoo': <function A.getfoo at 0x0000017674C58C80>, '__init__': <function A.__init__ at 0x0000017674C58D08>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

@staticmethod的描述器版本实现

class Staticmethod:

    def __init__(self,fn):
        self._fn = fn

    def __get__(self, instance, owner):
        return self._fn

class Foo:

    @Staticmethod # show = Staticmethod(show) 相当于类属性show,被类Staticmethod描述了。返回值为__get__的返回值。
    def show():
        print('I am staticmethod')

f = Foo()
f.show()

@classmethod的描述器版本实现

from functools import partial

class ClassMethod:
    def __init__(self,fn):
        self._fn = fn

    def __get__(self, instance, owner):
        return partial(self._fn,owner)

class Bar:
    @ClassMethod # show = ClassMethod(show)
    def show(cls):
        print(cls.__name__)

Bar.show()

参数类型检查功能

描述器方式实现

class Typed:
    def __init__(self,arg,typed):
        self.arg = arg
        self.typed = typed

    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self

    def __set__(self, instance, value):
        if not isinstance(value,self.typed):
            raise Exception('type error')
        instance.__dict__[self.arg] = value


import inspect
def typeassert(cls):
    obj = inspect.signature(cls).parameters
    for name,value in obj.items():
        if value.annotation !=  value.empty:
            setattr(cls,name,Typed(name, value.annotation))
    return cls


@typeassert  # Person = typeassert(Person)
class Person:
    #name = Typed('name',str)
    #age = Typed('age',int)
    def __init__(self,name:str, age:int):
        self.name = name
        self.age = age



tom = Person('tom',100)
print(tom.__dict__)

类装饰器方式实现

class Typed:
    def __init__(self,arg,typed):
        self.arg = arg
        self.typed = typed

    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self

    def __set__(self, instance, value):
        if not isinstance(value,self.typed):
            raise Exception('type error')
        instance.__dict__[self.arg] = value


import inspect
# def typeassert(cls):
#     obj = inspect.signature(cls).parameters
#     for name,value in obj.items():
#         if value.annotation !=  value.empty:
#             setattr(cls,name,Typed(name, value.annotation))
#     return cls

class TypeAssert:
    def __init__(self,cls):
        obj = inspect.signature(cls).parameters
        for name,value in obj.items():
            if value.annotation != value.empty:
                setattr(cls,name,Typed(name,value.annotation))
        self.cls = cls

    def __call__(self, name, age):
        return self.cls(name,age)



@TypeAssert  # Person = TypeAssert(Person)
class Person:
    #name = Typed('name',str)
    #age = Typed('age',int)
    def __init__(self,name:str, age:int):
        self.name = name
        self.age = age


tom = Person('tom',100)
print(tom.__dict__)