描述器
什么是描述器?
一个类中定义了如下一个或多个魔术方法,这个类的实例就是描述器:
__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 的访问也拦截)。
getattribute 和 getattr 又是做什么的?下文讲。
[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的 setattr 或 delattr。
读取 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