描述器
魔术方法:__get__()__set__()__delete__()__set_name__()
Python中的描述器

描述器


描述器涉及的魔术方法

用到3个魔术方法: __get__()__set__()__delete__(),具体方法签名如下:

  • object.__get__(self, instance, owner)
  • object.__set__(self, instance, value)
  • object.__delete__(self, instance)
  • self 指代当前实例,调用者
  • instance 是owner的实例(属主的实例)
  • owner 是属性的所属的类(属主)
    具体含义可参考下面的演示示例

引子

# 例1:
class A:
    def __init__(self):
        self.a = 100
        print('init class A ~~~')


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

print(B.x.a)
print('~' * 20)
b = B()
print(b.x.a)
--------输出结果-------
init class A ~~~
100
~~~~~~~~~~~~~~~~~~~~
init class B ~~~
100

并没有什么特殊的,也很容易理解。但是如果加上在A类中实现描述器涉及的魔术方法呢?

# 例2:
class A:

    def __init__(self):
        self.a = 100
        print('init class A ~~~')

    def __get__(self, instance, owner):
        print('in get func ~~~', self, instance, owner)  # self即为类A的实例,owner为属主,即类B,instance为owner(类B)的实例
        return self   # get方法需要给定一个返回值,这里为了方便B/b能够访问A实例的其他属性,一般返回A是实例,即self


class B:
    x = A()  # B类属性x为A的实例,且A类实现了__get__方法,访问类B或B类实例的属性x,则会去调用A类的__get__方法

    def __init__(self):
        print('init class B ~~~')


print(B.__dict__)  # x为类B的属性
print(B.x.a)  # 使用类来访问

print('-' * 30)

b = B()
print(b.__dict__)  # 为空,在实例字典中找不到,从实例的类字典找
print(b.x.a)  # 使用类实例来访问
---------------输出结果-------------------
init class A ~~~
{'__module__': '__main__', 'x': <__main__.A object at 0x1059e3438>, '__init__': <function B.__init__ at 0x105abd378>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
in get func ~~~ <__main__.A object at 0x1059e3438> None <class '__main__.B'>
100
------------------------------
init class B ~~~
{}
in get func ~~~ <__main__.A object at 0x1059e3438> <__main__.B object at 0x1059e35c0> <class '__main__.B'>
100
  • 类A在这里就是一个描述器(非数据描述器),实现了__get__方法
  • 如果类属性是类实例(即为描述器的实例),这个类被成为owner
# 例3:类实例的属性为描述器的类实例
class A:
    def __init__(self):
        self.a = 100
        print('init class A ~~~')

    def __get__(self, instance, owner):
        print('in get func ~~~', self, instance, owner)
        return self

class B:
    x = A()

    def __init__(self):
        print('init class B ~~~')
        self.x = A()    # x为类实例的属性

b = B()
print(b.__dict__)   # 在实例的字典中找x,能找到,则不会从类的字典中找
print(b.x.a)   # 并没有进入类A的get函数,也就是说,如果是类实例的属性,没有该表现
---------------输出结果------------------
init class A ~~~
init class B ~~~
init class A ~~~
{'x': <__main__.A object at 0x10a83b588>}
100
  • 只有类属性是类实例时,通过类或者类实例访问该类属性才会转而调用描述器的__get__方法,如果为类实例的属性是描述器实例,没有该表现

描述器定义

Python中,一个类实现了 __get____set____delete__ 三个方法中的任何一个方法,就是描述器。实现这三个中的某些方法,就支持了描述器协议

  • 仅实现了 __get__ ,就是非数据描述符(non-data descriptor)
  • 实现了 __get____set__就是数据描述符(data descriptor)
  • 如果一个类的类属性设置为描述器实例,那么它被称为owner属主
  • 当该类的该类属性被查找、设置、删除时,就会调用描述器相应的方法

接下来,我们来进一步看看__set__方法在描述器中的作用

例4:数据描述器
class A:

    def __init__(self):
        self.a = 100
        print('init class A ~~~')

    def __get__(self, instance, owner):
        print('in get func ~~~', self, instance, owner)
        return self

    def __set__(self, instance, value):
        print('in set func ~~~~', self, instance, value)
        self.data = value
        # instance.x = value  # instance.x set值时会再次转而调用set,因此会RecursionError: maximum recursion depth exceeded
        instance.__dict__['x'] = value  # 使用字典的方式避免递归调用


class B:
    x = A()

    def __init__(self):
        print('init class B ~~~')
        # 为实例增加属性x,且刚好为类属性的同名属性(x),且该类属性又是数据描述器实例(即实现了set),则会转而调用set方法
        self.x = 'b.x'   # 如果为非数据描述器,则仅仅是类B的实例的属性而已,参考例3


print(B.x)
print(B.x.a)

print('-' * 30)

b = B()
print(b.__dict__)
print(b.x)  # 尽管b的字典中此时有x属性,但是描述器的优先级更高,转而调用get,返回A类实例对象
print('-' * 30)
print(b.x.data)
print('-' * 30)
print(b.x.a)

print('~' * 30)
b.x = 100
print(b.__dict__)
print(b.x)

print('~'*30)
B.x = 200  # 此时B类属性被替换,B的类属性不再是描述器实例
print(b.__dict__)
print(b.x)
-------------输出结果-----------------------
init class A ~~~
in get func ~~~ <__main__.A object at 0x100512b70> None <class '__main__.B'>
<__main__.A object at 0x100512b70>
in get func ~~~ <__main__.A object at 0x100512b70> None <class '__main__.B'>
100
------------------------------
init class B ~~~
in set func ~~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> b.x
{'x': 'b.x'}
in get func ~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> <class '__main__.B'>
<__main__.A object at 0x100512b70>
------------------------------
in get func ~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> <class '__main__.B'>
b.x
------------------------------
in get func ~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> <class '__main__.B'>
100
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
in set func ~~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> 100
{'x': 100}
in get func ~~~ <__main__.A object at 0x100512b70> <__main__.B object at 0x100578588> <class '__main__.B'>
<__main__.A object at 0x100512b70>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{'x': 100}
100
  • 如果类B实例的属性x的同名类属性x是数据描述器实例,则类B或者类实例b访问属性,则遵循描述器协议访问
  • 数据描述器 优先于 实例的 __dict__
  • 实例的 __dict__ 优先于 非数据描述器
  • py3.6新增__set_name__方法,通过这个方法,就是可以知道属主类和属主类的类属性名
  • __set_name__方法会在属主类构建的时候调用
# 例:__set_name__()
class A:

    def __init__(self):
        self.a = 100
        print('init class A ~~~')

    def __get__(self, instance, owner):
        print('in get func ~~~', self, instance, owner)
        return self

    def __set__(self, instance, value):
        print('in set func ~~~~', self, instance, value)
        self.data = value
        instance.__dict__['x'] = value

    def __set_name__(self, owner, name):
        print('in set_name func ~~~', owner, name)
        self.name = name


class B:
    x = A()  # 类属性创建时调用描述器的__set_name__方法

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


print('-' * 30)
print(B.x)
print(B.x.name)
print('-' * 30)
b = B()
print(b.x)
print(b.x.name)
--------------输出结果-------------
init class A ~~~
in set_name func ~~~ <class '__main__.B'> x
------------------------------
in get func ~~~ <__main__.A object at 0x104335588> None <class '__main__.B'>
<__main__.A object at 0x104335588>
in get func ~~~ <__main__.A object at 0x104335588> None <class '__main__.B'>
x
------------------------------
init class B ~~~
in set func ~~~~ <__main__.A object at 0x104335588> <__main__.B object at 0x104407f28> b.x
in get func ~~~ <__main__.A object at 0x104335588> <__main__.B object at 0x104407f28> <class '__main__.B'>
<__main__.A object at 0x104335588>
in get func ~~~ <__main__.A object at 0x104335588> <__main__.B object at 0x104407f28> <class '__main__.B'>
x

Python中的描述器

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

  • Python的方法(包括staticmethod()和classmethod())都实现为非数据描述器。实例可以重新定义和覆盖方法。这允许单个实例获取与同一类的其他实例不同的行为
  • property()函数实现为一个数据描述器。因此,实例不能覆盖属性的行为
# 例1:python中的数据描述器,@property
class A:

    def __init__(self):
        self.__x = 100

   @property
    def x(self):
        return self.__x

a = A()
print(a.x)
a.x = 100
-------输出结果---------
100
Traceback (most recent call last):
  File "/Users/tina/PycharmProjects/airtest_test/t2.py", line 13, in <module>
    a.x = 100
AttributeError: can't set attribute

怎么理解property 装饰器实现了数据描述器呢?看如下例子

# 例2:
class A:

    def __init__(self):
        self.a = 100
        print('init class A ~~~')

    def __get__(self, instance, owner):
        print('in get func ~~~', self, instance, owner)
        return self

    def __set__(self, instance, value):
        print('in set func ~~~~', self, instance, value)
        if instance:
            raise AttributeError("can't set attribute")


class B:
    x = A()

    def __init__(self):
        print('init class B ~~~')

b = B()
print(b.x)
b.x = 100 # AttributeError: can't set attribute
------------输出结果---------------
Traceback (most recent call last):
  File "/Users/tina/PycharmProjects/airtest_test/t2.py", line 25, in <module>
    b.x = 100
  File "/Users/tina/PycharmProjects/airtest_test/t2.py", line 14, in __set__
    raise AttributeError("can't set attribute")
AttributeError: can't set attribute
...
...

由此可见,通过数据描述器,可以控制修改实例属性,对比参考例1

  • 描述器的其他应用场景:如对实例的数据进行校验(类型校验等)

参考

  • magedu