描述器Descriptors
文章目录
描述器的表现
用到3个方法:__get__()``__set__()``__delete__()
方法签名如下:
obj__get__(self,instance,owner)
obj__set__(self,instance,value)
obj__delete__(self,instance)
- self指代当前实例,调用者
- instance时owner的实例
- owner是属性所属的类
请思考下面代码执行流程是什么?
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
class B:
x = A()
def __init__(self):
print('B.init')
print('-' * 30)
b = B()
print(b.x.a1,B.x.a1)
#执行结果如下
A.init
------------------------------
B.init
a1 a1
类加载的时候变量需要先生成,而类B的x属性是类A的实例,所以类A先初始化,所以打印A.init。实例化b后,该实例和类都可以成功访问到x指向的实例。
如果在类A中实现了__get__()
方法,看看变化
class A:
def __init__(self):
self.a1 = 'a1'
def __get__(self,instance,owner):
print(self,instance,owner)
class B:
x = A()
def __init__(self):
pass
b = B()
print(B.x)
print(b.x)
#执行结果如下
<__main__.A object at 0x0000013A521D8F98> None <class '__main__.B'>
None
<__main__.A object at 0x0000013A521D8F98> <__main__.B object at 0x0000013A521D8EF0> <class '__main__.B'>
None
因为定义了__get__()
方法,类A就是一个描述器,使用类B或者类B的实例来对属性读取,就是对类A的实例的访问,就会调用该方法。如果需要通过该方法调用self实例的属性,则需要return self。当用类B访问时,instance的值为None,用类B的实例访问时,instance的值为类B的实例
那么类B的实例属性也可以这样吗?
class A:
def __init__(self):
self.a1 = 'a1'
def __get__(self,instance,owner):
print(self,instance,owner)
return self
class B:
x = A()
def __init__(self):
self.x = A()
self.b1 = A()
b = B()
print(B.x.a1)
print(b.x.a1)
print(b.b1.a1)
#执行结果如下
<__main__.A object at 0x0000013A521D1908> None <class '__main__.B'>
a1
a1
a1
由此可见,只有B类属性是A类的实例才行。
描述器定义
python中,一个类实现了__get__()
,__set__()
,__delete__()
三个方法中的任何一个方法,就是描述器。实现这三个中的某些方法,就支持了描述器协议。
- 仅实现了
__get__()
,就是非数据描述器 non-data descriptor - 实现了
__get__()
,__set__()
或者__delete__()
就是数据描述器 data descriptor
如果一个类的类属性设置为描述器实例,那么它就被称为owner属主(属主类)。
当该类的该属性被查找,设置,删除时,就会调用描述器相应的方法。
属性的访问顺序
先为类A增加__set__()
方法。
class A:
def __init__(self):
self.a1 = 'a1'
def __get__(self,instance,owner):
#print(self,instance,owner)
return self
def __set__(self,instance,value):
print('set----------',self,instance,value)
class B:
x = A()
def __init__(self):
self.b1 = 5
self.x = 5
b = B()
print(b.__dict__)
#执行结果如下
set---------- <__main__.A object at 0x0000013A521C3F98> <__main__.B object at 0x0000013A5223D0B8> 5
{'b1': 5}
为类A增加__set__()
方法后,类B的实例创建属性时,因为实例属性x和类B属性x同名,结果被类A的set方法拦截下来,所以看到实例b的字典里只有b1属性。
由此得出实例属性查找优先级:
数据描述器 > 实例字典 > 非数据描述器
注意:
b.x = 500,该方法会调用描述器的set
B.x = 500,该方法重新定义了类的x属性,之前的描述器实例就被覆盖了
__delete__()
方法有同样的效果,有了这个方法,也是数据描述器。
class A:
def __init__(self):
self.a1 = 'a1'
def __get__(self,instance,owner):
return self
def __delete__(self,instance):
print('del',self,instance)
class B:
x = A()
def __init__(self):
pass
b = B()
del b.x
del B.x
B.__dict__
#执行结果如下
del <__main__.A object at 0x0000022E62491DD8> <__main__.B object at 0x0000022E62491EF0>
mappingproxy({'__module__': '__main__',
'__init__': <function __main__.B.__init__(self)>,
'__dict__': <attribute '__dict__' of 'B' objects>,
'__weakref__': <attribute '__weakref__' of 'B' objects>,
'__doc__': None})
上例得出,删除B.x后,类B的字典里就没有x属性了,所以描述器只有__get__
方法对属主类调用起作用。
python中的描述器
描述器在python中应用非常广泛
python的方法(包括staticmethod和classmethod)都实现了非数据描述器。因此,实例可以重新定义和覆盖方法,这允许单个实例获取以同一类的其他实例不同的行为。
property()函数实现了一个数据描述器,因此,实例不能覆盖属性的行为。
class A:
@classmethod
def f1():#非数据描述器
pass
@staticmethod
def f2():#非数据描述器
pass
def f3():#非数据描述器
pass
@property
def f4():#-----数据描述器-----
pass
def __init__(self):#非数据描述器
self.f1 = 1
self.f2 = 2
self.f3 = 3
#self.f4 = 4#报属性错误AttributeError: can't set attribute
a = A()
print(A.__dict__)
print(a.__dict__)
##执行结果如下
{'__module__': '__main__', 'f1': <classmethod object at 0x0000013A5223D208>, 'f2': <staticmethod object at 0x0000013A5223D828>, 'f3': <function A.f3 at 0x0000013A521D6B70>, 'f4': <property object at 0x0000013A52236A48>, '__init__': <function A.__init__ at 0x0000013A521F4B70>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'f1': 1, 'f2': 2, 'f3': 3}
新增方法
3.6新增描述器方法__set_name__
,它在属主类构建的时候就会调用。
class A:
def __init__(self):
self.a1 = 'a1'
def __get__(self,instance,owner):
return self
def __set_name__(self,owner,name):
print(self,owner,name)
class B:
x = A()
def __init__(self):
pass
b = B()
##执行结果如下
<__main__.A object at 0x0000013A522322E8> <class '__main__.B'> x
提供该方法,就是可以知道属主类和属主类的类属性名。
练习
1.实现StaticMethod装饰器
from functools import wraps
class StaticMethod:
def __init__(self, fn ):
self.fn = fn
wraps(fn)(self)
def __get__(self,instance,owner):
return self.fn
class Person:
@StaticMethod#该装饰器将foo提取出作为实参传入,并返回一个实例
def foo(x, y ):#foo = StaticMethod(foo) => StaticMethod instance
print('foo',x + y)
p = Person()
Person.foo(1,2)
p.foo(3,4)
##执行结果如下
foo 3
foo 7
2.实现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 Person:
@ClassMethod#该装饰器将foo提取出作为实参传入,并返回一个实例
def foo(cls,x, y ):#foo = StaticMethod(foo) => StaticMethod instance
print('foo',cls.__name__,x + y)
p = Person()
p.foo(5,6)
##执行结果如下
foo Person 11
3.对下列实例的数据进行校验(使用装饰器和描述器)
class Person:
def __init__(self,name:str,age:int):
self.name = name
self.age = age
使用硬方法
class Person:
def __init__(self,name:str,age:int):
tar = ((name,str),(age,int))
for n,t in tar:
if not isinstance(n,t):
print(f'{n} TypeError')
#raise TypeError(f'{n}')
self.name = name
self.age = age
p = Person(18,'tom')
p.__dict__
print('-' * 30)
p1 = Person('tom',18)
p1.__dict__
##执行结果如下
18 TypeError
tom TypeError
------------------------------
{'name': 'tom', 'age': 18}
这样把代码写死了,不好!
使用描述器
class AttrCheck:
def __init__(self,name,typ):
self.name = name
self.typ = typ
def __get__(self,instance,owner):
if not instance:
raise TypeError(f'access denied: {owner.__name__}')
return instance.__dict__[self.name]
def __set__(self,instance,value):
print('target:{}--actual:{} value:{}'.format(self.typ,type(value),value))
if isinstance(value,self.typ):
instance.__dict__[self.name] = value
class Person:
name = AttrCheck('name',str)
age = AttrCheck('age',int)
def __init__(self,name:str,age:int):
self.name = name
self.age = age
p = Person(18,'tom')
print('-' * 30)
p1 = Person('tom',18)
print('-' * 30)
print(p1.__dict__)
p1.name
#Person.name#get方法中实现了通过属主类访问异常所以,该代码报错
##执行结果如下
target:<class 'str'>--actual:<class 'int'> value:18
target:<class 'int'>--actual:<class 'str'> value:tom
------------------------------
target:<class 'str'>--actual:<class 'str'> value:tom
target:<class 'int'>--actual:<class 'int'> value:18
------------------------------
{'name': 'tom', 'age': 18}
'tom'
使用装饰器函数加描述器
import inspect
class AttrCheck:
def __init__(self,name,typ):
self.name = name
self.typ = typ
def __get__(self,instance,owner):
if not instance:
raise TypeError(f'access denied: {owner.__name__}')
return instance.__dict__[self.name]
def __set__(self,instance,value):
print('target:{}--actual:{} value:{}'.format(self.typ,type(value),value))
if isinstance(value,self.typ):
instance.__dict__[self.name] = value
def attrchect(cls):
sig = inspect.signature(cls)
parm = sig.parameters
for v in parm.values():
if not v.annotation is inspect._empty:
print(v.name,v.annotation)
setattr(cls,v.name,AttrCheck(v.name,v.annotation))
return cls
@attrchect
class Person:
def __init__(self,name:str,age:int):
self.name = name
self.age = age
Person.__dict__
p = Person(18,'tom')
print(p.__dict__)
print('-' * 30)
p1 = Person('tom',18)
print(p1.__dict__)
p1.name,p1.age
##执行结果如下
name <class 'str'>
age <class 'int'>
target:<class 'str'>--actual:<class 'int'> value:18
target:<class 'int'>--actual:<class 'str'> value:tom
{}
------------------------------
target:<class 'str'>--actual:<class 'str'> value:tom
target:<class 'int'>--actual:<class 'int'> value:18
{'name': 'tom', 'age': 18}
('tom', 18)
通过装饰器函数给类Person动态添加了两个属性(属性名根据init函数签名获得),这样就避免了写死代码
nt(p1.dict)
p1.name,p1.age
##执行结果如下
name <class ‘str’>
age <class ‘int’>
target:<class ‘str’>–actual:<class ‘int’> value:18
target:<class ‘int’>–actual:<class ‘str’> value:tom
{}
target:<class ‘str’>–actual:<class ‘str’> value:tom
target:<class ‘int’>–actual:<class ‘int’> value:18
{‘name’: ‘tom’, ‘age’: 18}
(‘tom’, 18)
通过装饰器函数给类Person动态添加了两个属性(属性名根据init函数签名获得),这样就避免了写死代码