描述器
定义:
Python中如果一个类实现了 __get__、__set__、__delete__ 三个方法中的任意一个,那么这个类就是一个描述器。
如果仅实现了 __get__就是非数据描述器non data descriptor;
同时实现了 __get__、__set__ 就是数据描述器 data descriptor;
如果一个类的类属性设置为描述器,那么它被称为owner属主。method也是类的属性
__get__方法的触发时机:
如果一个类的类属性是一个实例,并且这个实例是一个描述器,当访问这个类属性时,就会触发描述器中的__get__方法。ps:通过实例属性访问,不会触发__get__方法
# 定义一个非数据描述器
class A:
def __init__(self):
print('A init')
def __get__(self, instance, owner):
print('__get__', self, instance, owner)
return self
class B:
# 类加载时就已经创建,
x = A() # 类属性是一个描述器
def __init__(self):
print('B init')
self.x = A()
print(B.x) # 访问类属性,触发__get__方法
print(B().x) # 访问实例属性,不会触发__get__方法,即使实例属性名和类属性相同,非数据描述器不会触发
输出:
A init
__get__ <__main__.A object at 0x0000018F48169C50> None <class '__main__.B'>
<__main__.A object at 0x0000018F48169C50>
B init
A init
<__main__.A object at 0x0000018F48379C50>
__get__方法参数说明:
instance:属主类B的实例
owner:属主类B
返回值:类属性x的值
__set__方法的触发时机:
如何一个类的类属性是一个数据描述器(同时有__get__ 和 __set__ 方法),且这个类的实例属性名称和这个类属性具有相同的名字,当访问这个类和类属性同名的类实例时,实际访问的是这个类的类属性,会同时触发__get__ 和 __set__ 方法
# 定义一个数据描述器
class A:
def __init__(self):
print('A init')
def __get__(self, instance, owner):
print('__get__', self, instance, owner)
return self
def __set__(self, instance, value):
print('__set__', self, instance, value)
class B:
# 类加载时就已经创建,
x = A() # 类属性是一个数据描述器
def __init__(self):
print('B init')
self.x = 100 # 实例名字和类属性名字相同
# 访问实例属性,实际访问的类属性,会先触发__set__方法在触发__get__方法
print(B().x)
输出:
A init
B init
__set__ <__main__.A object at 0x000001A66DD19C50> <__main__.B object at 0x000001A66DF29DD8> 100
__get__ <__main__.A object at 0x000001A66DD19C50> <__main__.B object at 0x000001A66DF29DD8> <class '__main__.B'>
<__main__.A object at 0x000001A66DD19C50>
B().x = 1 在外部操作时,同样会触发__set__方法
官方解释:
当类属性是一个数据描述器,且实例属性和类属性名称相同,访问类实例属性时,属性查找顺序,类字典的优先级高于实例字典优先级,所以因先在类字典进行搜索
本质:
通过打印 B.__dict__ 和 B().__dict__ 发现,B().__dict__中没有定义实例属性,所以在访问时发现实例字典中没有,会在类中进行搜索,最终访问的是类属性
print(B().__dict__)
print(B.__dict__)
{}
{'__weakref__': <attribute '__weakref__' of 'B' objects>, 'x': <__main__.A object at 0x000001D23EA29C88>, '__dict__': <attribute '__dict__' of 'B' objects>, '__module__': '__main__', '__doc__': None, '__init__': <function B.__init__ at 0x000001D23EA69E18>}
描述器在Python中应用非常广泛
Python中的方法(包括staticmethod 和 classmethod )都实现了非数据描述器,因此,实例可以重新定义和覆盖方法,这允许单个实例获取与同一个的其他实例不同的行为。
Property类实现了一个数据描述器,因此,实例不能覆盖属性的行为。
应用实例:
① 自定义类中的静态方法staticmethod
分析过程:
1、 自定义一个类 StaticMethod;
2、 用于装饰另外一个A类中的foo方法,相当于foo = StaticMethod(foo);
3、 所以在StaticMethod的初始化方法中需要接受foo函数,__init__(self, fn);
4、 foo方法本身为A类中类属性,值为StaticMethod类的实例,如果StaticMethod为非数据描述时,访问foo时,会触发StaticMethod类中的__get__方法;
5、 __get__返回值为foo方法本身,这样在执行foo()时,才能指向foo本身
代码如下:
class StaticMthod:
def __init__(self, fn):
self._fn = fn
def __get__(self, instance, owner):
print('__get__: ', self, instance, owner)
return self._fn # 返回fn本身
class A:
@StaticMthod # 相当于 foo = StaticMthod(foo)
def foo():
print('static')
# foo是A的类属性,值为StaticMthod实例,且StaticMthod是一个描述器
# 访问A.foo时,会触发__get__方法,返回值为A.foo的值
a = A.foo # 此时就会触发__get__方法
a() # 执行
输出:
__get__: <__main__.StaticMthod object at 0x000001FE6A189CC0> None <class '__main__.A'>
static
② 自定义类中的类方法classmethod
分析过程和静态方法类似,classmethod执行是多一个cls参数,如果__get__ 返回值还是self._fn的话,就会报错,缺少一个参数,所以引入偏函数将cls进行固化,返回一个新的可调用对象
示例代码:
from functools import partial
class ClassMethod:
def __init__(self, fn):
self._fn = fn
def __get__(self, instance, owner):
print("__get__ : ", self, instance, owner)
return partial(self._fn, owner)
class A:
@StaticMthod # 相当于 foo = StaticMthod(foo)
def foo():
print('static')
@ClassMethod # 相当于 bar = StaticMthod(bar)
def bar(cls):
print(cls.__name__)
# foo是A的类属性,ClassMethod,ClassMethod是一个描述器
# 访问A.bar时,会触发__get__方法,返回值为A.bar的值
a = A.bar # 此时就会触发__get__方法
a() # 执行
输出:
__get__ : <__main__.ClassMethod object at 0x000001C7DE6C9DD8> None <class '__main__.A'>
A
③ 对实例的数据进行检查
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
思路:
写函数,在__init__中先检查,如果不合格,直接跑异常
装饰器,使用inpect模块完成
描述器
使用描述器实现:
分析:实例数据进行检查,即需要触发__set__ 方法,所以将类实例属性的名称和类属性的名称定义为相同。
第一版代码如下:
class Typed:
def __init__(self, name, _type):
self.name = name
self.type = _type
def __get__(self, instance, owner):
print('__get__:', self, instance, owner)
if instance is not None:
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
print('__set__:', self, instance, value)
if not isinstance(value, (self.type,)):
raise ValueError(value)
instance.__dict__[self.name] = value
class Person:
# 类属性名称和类实例属性定义为一样
name = Typed('name', str)
age = Typed('age', int)
def __init__(self, name: str, age: int):
# 初始化实例属性时会触发__set__方法
self.name = name
self.age = age
p = Person('lisi',18)
# 访问实例属性时会触发__get__方法
print(p.name)
print(p.age)
上面代码在类中需要定义和实例属性相同的名称,显然不够灵活
改进如下:
使用inspect模块动态获取初始化方法签名
使用装饰器的方式动态给类添加类属性
# 定义描述器
class Typed:
def __init__(self, name, _type):
self.name = name
self.type = _type
def __get__(self, instance, owner):
# print('__get__:', self, instance, owner)
if instance is not None:
# 访问属性时触发__get__,访问__set__访问的值
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
# print('__set__:', self, instance, value)
if not isinstance(value, (self.type,)):
raise ValueError(value)
# 将value保存在__dict__中
instance.__dict__[self.name] = value
# 装饰器类的装饰器
def typeassert(cls):
import inspect
params = inspect.signature(cls).parameters
for name, param in params.items():
# print(name, param.annotation)
if param.annotation != param.empty(): # 只检查注解不为空的参数
# 动态方式给类添加类属性,类属性值为描述器实例
# 将装饰器和描述器联系起来
setattr(cls, name, Typed(name, param.annotation))
return cls
@typeassert # Person = TypeAssert(Pseron), Person()
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
上面的装饰器还可以定义为类装饰器
class TypeAssert:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
import inspect
params = inspect.signature(self.cls).parameters
for name, param in params.items():
# print(name, param.annotation)
if param.annotation != param.empty():
setattr(self.cls, name, Typed(name, param.annotation))
return self.cls(*args, **kwargs) # 返回类实例