Python中的描述器、反射

Python Descriptor

Posted by BlueFat on Saturday, August 27, 2022

描述器

什么是描述器?

一个类中定义了如下一个或多个魔术方法,这个类的实例就是描述器:

__get__,__set__,__delete__

通常需要两个类来构建描述器:

如果类B的类属性x,指向另一个类A的实例。被指向的A的实例就是描述器对象。B.x是描述器,B也是描述器的属主(owner)。比如:

class A:
    def __get__(self, instance, owner):
        pass

    def __set__(self, instance, value):
        pass

    def __delete__(self, instance):
        pass

class B:
    x = A()  # x是描述器
    pass

类属性的值,通常是一些已有类型的对象,比如字符串、列表等。

当使用了描述器,类属性就指向一个描述器对象,描述器通过三个魔术方法,可以自定义属性的行为。

描述器的分类

非数据描述器:

只定义了__get__,就是非数据描述器(non-data descriptor)。

数据描述器:

定义了__get__,且定义了__set____set__ 与 __delete__,就是数据描述器(data descriptor)。

属性搜索顺序

当一个实例与它所属的类有相同的属性名时:

非数据描述器,实例的属性搜索顺序:

__getattribute__ ⟶ 默认搜索顺序 [1] ⟶ __getattr__。也就是说,此时__get__无效。

数据描述器,会拦截实例属性字典的访问:

不会访问实例属性字典__dict__。属性访问或修改会被描述器的__get__, __set__, __delete__方法处理。

注意:

如果有 getattribute 方法,不管有没有描述器,实例属性搜索时,都优先调用此方法,可以拦截一切(包括 实例.dict 的访问也拦截)。

getattributegetattr 又是做什么的?下文讲。

[1] 默认搜索顺序: 默认搜索顺序就是没有描述器时的搜索顺序,遵循如下规则: 实例的属性字典(dict) ⟶ 类的属性字典 ⟶ 类的父类的属性字典 ⟶ … ⟶ 祖先类object的属性字典

属性搜索顺序与类的继承有关。如果是单继承,属性(或方法)搜索路径是确定的,一直向上找。如果是多继承,就涉及到MRO(方法解析顺序)。Python3的MRO采用C3算法,在类被创建出来的时候,就计算出一个MRO有序列表。关于C3算法,见官方文档

属性读写操作示例

B.x = 400,类属性赋值(赋值即重新定义),如果x是描述器,将被覆盖。 b.x = 500,非数据描述器时,将修改实例自己的属性(dict)。 b.x = 600,数据描述器时,将调用描述器的__set__方法。

B.x,若x是描述器,调用描述器的__get__方法。 b.x,若x是描述器,调用描述器的__get__方法。

直接操作实例的__dict__字典,可以绕开描述器对__get____set__等的调用。

举例:

class A:
    def __init__(self):
        print('A().init')
        self.x = 101       # 这是A自己的实例,与B无关

    def __get__(self, instance, owner):
        print('~~~~ A.get ~~~~')
        print(self)         # A的实例本身
        print(instance)     # B.x访问时,为None。b.x访问时,为B的实例对象
        print(owner)        # 类B
        print('~~~~ A.get ~~~~')
        return getattr(instance, 'z', 'no_z_found')  # 查找b.z时,又会回去调用B.__getattribute__

    def __set__(self, instance, value):
        print('~~~~~ A.set ~~~~')
        print(self)         # A的实例本身
        print(instance)     # 修改b.x时,才进入此方法,instance为B的实例对象
        print(value)        # 赋给 b.x 的值
        print('~~~~~ A.set ~~~~')
        instance.z = value  # 演示,把b.x的值保存到b.z,而不是b的属性字典__dict__

    def __delete__(self, instance):
        print('~~~~~ in delete')
        del instance.z

class B:
    # 创建描述器x
    x = A()

    # 定义如下方法,实例属性访问最先调用它,但在类A中定义无用
    def __getattribute__(self, item):
        print('___ in getattribute ___')

        # 查找b.x时,此处又会调用A.__get__(因为x是描述器),而不是调B.__getattribute__,不然会递归
        return object.__getattribute__(self, item)

    def __init__(self):
        print('B().init')
        self.x = 1000       # 非数据描述器时,实例修改自己的属性,self.x会访问实例自己的__dict__
                            # 数据描述器时,调用 A.__set__

b = B()          # 先生成A的实例,即执行A.__init__,生成描述器对象,然后执行B.__init__

# 属性访问
print('\n' + '-' * 30)
print(B.x)
print()
print(b.x)

# 属性修改
# 覆盖描述器
#B.x = 123
#print(B.x)
print('\n' + '#' * 30)

# 非数据描述器时,b修改自己的属性,赋值即重新定义,覆盖描述器x
# 数据描述器时,b.x 调用 A.__set__
b.x = 456
print(b.x)

print('\n' + '=' * 30)
print(b.__dict__)  # 甚至访问b的属性字典,也是调用B.__getattribute__
del b.x            # 删除属性,会调用 A.__delete__
print(b.x)
print(b.__dict__)

反射

上文提到的__getattribute__跟__get__有什么关系呢?实际上前者是反射相关的魔术方法。那什么是反射呢?

当我们需要用到对象的某个属性(或方法),但是由于某种原因无法确定这个属性是否存在,这时我们需要用一种特殊的机制,去访问和操作这个未知的属性,这种机制就称为反射(reflection)。反射就相当于一种自我检查机制。

反射机制不仅包括,要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息,改变程序状态或结构。总之一句话,反射指的是运行时获取类型定义信息,并且还能修改这些信息。

与反射相关的四个函数:getattr、setattr、delattr、hasattr。与这些函数相关的四个魔术方法:__getattr__, __setattr__, __delattr__, __getattribute__。见下面表格:

魔术方法 含义
__getattr__ 此方法只影响实例。实例属性默认搜索顺序:实例自己(的__dict__,后同)、实例的类、类的父类、父类的父类、object祖先类。若从这个顺序中没有找到属性,会抛出AttributeError异常,但类中定义了__getattr__,实例将捕获异常,并调用此方法。此方法可用于实例没有找到属性时,拦截异常,做一些操作。
__setattr__ 此方法只影响实例。self.x = x, setattr(self, ‘x’, x) 等涉及到修改实例属性的操作时,如果定义了__setattr__,就会调用此方法。此方法可以拦截实例属性修改操作的默认行为。比如将实例的属性存储在新的字典中,而不是存储在默认字典__dict__。
__delattr__ 此方法只影响实例。del self.x, delattr(self, ‘x’) 等涉及删除实例属性的操作时,如果定义了__delattr__,将会调用此方法。
__getattribute__ 此方法只影响实例。实例的所有属性的访问,第一个就调用此方法。此方法能完全控制属性的默认访问顺序。可以在此方法中做一些处理,然后手动抛出AttributeError异常,这将继续调用__getattr__方法(如果有的话)。
__getattr__ VS __getattribute__

两者的执行时间点不同。

前者会在默认属性搜索顺序中未找到属性时,拦截异常,并执行。

后者会在第一时间执行,完全拦截默认属性搜索顺序。两者执行顺序如下:

__getattribute__``实例属性的默认搜索顺序``__getattr__

举例:

class A:
    def __init__(self, x, y):

        # 用kv赋值方式增加属性 不会调用__setattr__
        self.__dict__['a'] = 7
        self.__dict__['_d'] = {}

        # 如下属性将会存储到新字典_d 会调用__setattr__
        self.x = x
        self.y = y

    def __getattr__(self, item):
        print('_in getattr:', item)

        # 属性未找到时 才调用__getattr__ 并从新字典返回属性
        return self._d[item]

    def __setattr__(self, key, value):
        print('_in setattr:', key, value)

        # 下面写法都会递归 它们都调用__setattr__
        # self.key = value
        # setattr(self, str(key), value)

        # 用新字典存储属性
        # _d 属性在实例的__dict__,因为是字典操作,所以等号左边的self._d就不会调用__getattr__
        self._d[key] = value

    def __delattr__(self, item):
        print('_in delattr:', item)
        del self._d[item]

a = A(4, 5)
print(a.__dict__)  # 打印属性字典,不会调用A.__getattr__,除非定义了_getattribute__
print(a.x)
del a.x
delattr(a, 'y')
a.t = 123          # 这也会调__setattr__
print(a.__dict__)
print('=' * 30)

class A:
    d = {}
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __getattribute__(self, item):
        print(item, '~~~~~~~')

        # 推荐写法
        return object.__getattribute__(self, item)

a = A(3, 4)
print('#' * 30)
print(a.x, a.d)

如果上面提及的魔术方法同时存在,会怎么样呢?

下面是所有魔术方法同时出现的情况下,类或实例的属性搜索顺序

实例属性搜索顺序 还是以 a.x 为例。

修改/删除 a.x 时

不管 x 这个属性是不是描述器,描述器不起作用:

  • 修改a.x时,直接调 setattr
  • 删除a.x时,直接调 delattr

因为修改/删除肯定是操作a自己的x,不会去操作继承位置的x,显然是直接调用类A的 setattrdelattr

读取 a.x 时

如果 x 是描述器:

  • 1、非数据描述器时,描述器不起作用: getattribute ⟶ 实例默认搜索顺序 ⟶ getattr

  • 2、数据描述器时: getattribute ⟶ 描述器的__get__

如果 x 不是描述器:

  • getattribute ⟶ 实例默认搜索顺序 ⟶ getattr

类属性搜索顺序

修改/删除 A.x

  • 与实例一样,是操作A自己的x,不存在搜索顺序。

  • 修改时,比如A.x=100,赋值即重新定义。

读取 A.x

  • 如果 x 是描述器:调描述器的 get

  • 如果不是描述器:符合上文的类属性的默认搜索顺序

描述器的应用

用描述器实现ClassMethod、StaticMethod(非数据描述器的应用):

# 非数据描述器 实现StaticMethod
class StaticMethod(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        print('_in StaticMethod')
        print(self, instance, owner)
        return self.fn


# 非数据描述器 实现ClassMethod
class ClassMethod(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        print('_in ClassMethod')
        print(self, instance, owner)
        return partial(self.fn, owner)  # 固定fn的owner参数,就是固定bar函数的所属类A2


class A2(object):
    AGE = 20

    def __init__(self, name, age):
        self.name = name
        self.__age = age

    # 装饰器语法
    @StaticMethod  # foo=StaticMethod(foo) 构建非数据描述器对象foo
    def foo(x):
        print('_in foo')
        return x

    # 装饰器语法
    @ClassMethod  # bar=ClassMethod(bar) 构建非数据描述器对象bar
    def bar(cls, x):
        print('_in bar')
        return cls.AGE, x

a = A2('Tom', 19)

# 这里分两步,a.foo读取属性foo,会调用__get__返回A2里定义的foo函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(3),以此来实现静态方法foo
print(a.foo(3))
print('=' * 30)

# 这里分两步,a.bar读取属性bar,会调用__get__返回A2里定义的bar函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(4),cls参数已固定,实现了类方法
print(a.bar(4))

用描述器实现Property(数据描述器的应用):

class Property(object):
    def __init__(self, fget, fset=None, fdel=None):
        self.fget = fget  # fget是A的方法age
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        # fget是self实例的属性,不是Property的方法,所以不会把self自动传递给fget,所以需要instance参数
        return self.fget(instance)

    def __set__(self, instance, value):
        self.fset(instance, value)

    def __delete__(self, instance):
        self.fdel(instance)

    def setter(self, fset):
        self.fset = fset
        return self  # 必须返回Property实例才能构建描述器

    def deleter(self, fdel):
        self.fdel = fdel
        return self


class A(object):
    def __init__(self):
        self.__age = 13

    # age=Property(age)=描述器对象
    # @Property必需在@age.setter与@age.deleter的前面
    # 因为@Property创建了描述器对象add,下面才能使用add对象
    @Property
    def age(self):
        return self.__age

    # age=age.setter(age)=描述器对象
    # 因为age是描述器对象,指向Property的实例,该实例有属性setter
    @age.setter
    def age(self, value):
        self.__age = value

    @age.deleter
    def age(self):
        del self.__age


a = A()
print(a.age)  # 把方法调用变成了属性访问
a.age = 17    # 调__set__
print(a.age)
del a.age     # 调__delete__

https://izhaojie.com/2021/08/19/python-descriptor.html