1 描述器
一般来说,一个描述器是一个有绑定行为
的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。
有这些方法的对象叫做描述器。
默认对属性的访问控制是从对象的字典里面(__dict__)中获取、设置和删除它。举例来说, 比如 a.x 的查找顺序是, a.__dict__['x'] , 然后 type(a).__dict__['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)). 如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)。
2 描述器协议
描述器主要涉及三个方法:
- descr.__get__(self, obj, type=None) --> value
- descr.__set__(self, obj, value) --> None
- descr.__delete__(self, obj) --> None
一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找、设置和删除行为。
2.1 非数据描述器
在类中仅仅定义了__get__方法的描述器被称为非数据描述器(non-data descriptor)。
非数据描述器的优先级低于实例的__dict__。
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
def __get__(self, instance, owner):
pass
# return self
class B:
x = A()
def __init__(self):
print('B.init')
print('-' * 20)
b = B()
print(b.x.a1)
# Traceback (most recent call last):
# A.init
# --------------------
# B.init
# File "E:/Python - base - code/ClassDesc.py", line 20, in <module>
# print(b.x.a1)
# AttributeError: 'NoneType' object has no attribute 'a1'
分析:
- Class A实现了__get__方法,所以它是一个非数据描述器
- 由于Class B里面设置的x属性是Class A的实例,所以在定义阶段就会实例化,把实例化的对象赋给x属性,所以会先执行A的__init__方法。
- 访问实例b的x属性时,发现值是一个描述器,然后就会被描述器A的__get__方法捕获
- __get__方法默认然会None,所以None对象没有a1属性,所以报属性错误
- 在__get__方法中,return self就可以访问了
那么self是什么,__get__方法的参数都是什么意思:
self
:对应A的实例(这里是属性x)owner
:对应的是x属性的拥有者,也就是B类instance
:它的值有两个- 当使用owner类直接调用时,
它是None
- 当使用owner类的实例调用是,是
实例本身
- 当使用owner类直接调用时,
2.1.1 实例分析
下面是小例子,分析代码结果
class Person:
def __init__(self):
self.country = 'Earth'
def __get__(self, instance, owner):
return self.country
class ChinaPeople:
country = Person()
def __init__(self,name,country):
self.name = name
self.country = country
daxin = ChinaPeople('daxin','China')
print(daxin.country)
分析:
- 由于描述器Person是一个非数据描述器,优先级低于实例自己的__dict__
- 实例在初始化时对self.country属性赋值,会直接在自己的__dict__中,写入{"country":"China}
- 在访问时根据MRO,优先访问实例自己的__dict__,所以结果是China
2.2 数据描述器
同时实现了__get__、__set__方法就称为数据描述器(data descriptor)
数据描述器的优先级高于实例的字典__dict__。
class A:
def __init__(self):
self.name = 'A'
def __get__(self, instance, owner):
print('From A __get__')
return self.name
def __set__(self, instance, value):
print('From A __set__')
class B:
name = A()
def __init__(self):
self.name = 'B'
b = B()
print(b.name)
# 结果:
# From A __set__
# From A __get__
# A
分析:
- 描述器A,实现一个数据描述器,优先级高于实例的自己的__dict__
- 在对b进行实例化的时候,设置了b的name属性,根据mro规则,找到父类B的name属性,然后发现其是一个数据描述器,然后被描述器A的__set__方法捕获。
- 当打印实例属性name时,由于数据描述器中,没有对传入的'B'进行赋值,所以这里'B'就丢了,最后访问属性name,会被描述器的__get__方法捕获,并返回描述器的name属性,所以打印是"A"
那么self是什么,__set__方法的参数都是什么意思:
self
:对应A的实例(这里是属性name)instance
:对应的是实例本身,这里就是bvalue
:表示设置的值(这里就是'B')
2.2.1 实例
分析下面代码的运行原理
class A:
def __init__(self):
self.name = 'A'
def __get__(self, instance, owner):
print('From A __get__')
# return self.name
return instance.__dict__['name']
def __set__(self, instance, value):
print('From A __set__')
instance.__dict__['name'] = value
class B:
name = A()
def __init__(self):
self.name = 'B'
b = B()
print(b.name)
分析:
- 当b在初始化时,对name属性进行了设置,所以第一步先按照mro查找name属性。
- 在父类B中,查找到类属性name,它的结果是一个数据描述器,所以设置的请求被数据描述器的__set__方法捕获,在__set__方法中,为实例自己的__dict__注入了属性name以及它的值。
- 在打印name属性时,由于数据描述器的优先级高于实例的__dict__,所以操作被描述器的__get__方法捕获,在内部返回了实例自己__dict__的属性name,所以最后打印'B'
2.3 描述器的调用及属性访问顺序
当类中存在描述器时,那么对象属性的调用就会发生变化。根据上面的例子,我们知道,实例属性访问的优先级为:数据描述器 > 实例字典__dict__ > 非数据描述器
特别注意:这里的访问顺序指的是:
实例属性对应一个描述器时的顺序。
,如果直接对类属性进行赋值操作,会直接覆盖类的描述器。
结合前面学的魔术方法,分析整个过程。
- 实例daxix的属性name(daxin.name) 本质上执行的是daxin.__getattribute__()方法
- daxin.__getattribute__() 其实是type(daxin).__dict__['name'].__get__(daxin,type(daxin))
使用Pyhon描述这个过程就是
def __getattribute__(self, key):
print('from B __getattribute__')
v = super(B, self).__getattribute__(key) # 这里用 self.__getattribute__就会递归了
# v = object.__getattribute__(self, key) # 使用super的方法,等同于直接调用object
if hasattr(v, '__get__'):
return v.__get__(self, type(self))
return v
完整的代码:
class A:
def __init__(self):
self.name = 'A'
def __get__(self, instance, owner):
print('From A __get__')
# return self.name
return instance.__dict__['name']
def __set__(self, instance, value):
print('From A __set__')
instance.__dict__['name'] = value
class B:
name = A()
def __init__(self):
self.name = 'B'
def __getattribute__(self, key):
print('from B __getattribute__')
v = super(B, self).__getattribute__(key)
if hasattr(v, '__get__'):
return v.__get__(self, type(self))
return v
b = B()
print(b.name)
2.4 描述器总结
总结几点比较重要的:
- 描述器的调用是因为__getattribute__()方法
- 重写__getattribute__()方法会组织正常的描述器调用
- __getattribute__()只对新式类的实例可用
- object.__getattribute__()和type.__getattribute__()对__get__()的调用不一样
- 数据描述器总是比实例字典优先
- 非数据描述器可能被实例字典重写/覆盖(非数据描述器不如实例字典优先)
3 Python的描述器体现
描述器在Python中应用非常广泛。我们定义的实例方法,包括类方法(classmethod)和静态方法(staticmethod)都属于非数据描述器。所以实例可以重新定义和覆盖方法。这样就可以使一个实例拥有与其他实例不同的行为(方法重写)。
但property装饰器不然,它是一个数据描述器,所以实例不能覆盖属性。
class A:
def __init__(self,name ):
self._name = name
@staticmethod
def hello(): # 非数据描述器
print('world')
@classmethod
def world(cls): # 非数据描述器
print('world')
@property
def name(self): # 数据描述器
return self._name
def welcome(self): # 非数据描述器
print('Welcome')
class B(A):
def __init__(self,name):
super().__init__(name)
daxin = B('daxin')
daxin.hello = lambda : print('modify hello') # 可以被覆盖
daxin.world = lambda : print('modify world') # 可以被覆盖
daxin.welcome = lambda : print('modify welcome') # 可以被覆盖
daxin.name = lambda self: self._name # 无法被覆盖
daxin.hello()
daxin.world()
daxin.welcome()
3.1 staticmethod简单实现
下面是一个简单的StaticMethod的实现
class StaticMethod:
def __init__(self, fn):
self.fn = fn
def __get__(self, instance, owner):
return self.fn
class A:
@StaticMethod # hello = StaticMethod(hello)
def hello():
print('hello world')
daxin = A()
daxin.hello() # hello() = StaticMethod().fn()
静态方法不需要传参,那么只需要在__get__方法拦截后,仅仅返回方法本身即可。
3.2 ClassMethod简单实现
import functools
class ClassMethod:
def __init__(self, fn):
self.fn = fn
def __get__(self, instance, owner):
#return lambda : self.fn(owner)
return functools.partial(self.fn,owner)
class A:
@ClassMethod
def hello(cls):
print('hello world {}'.format(cls.__name__))
daxin = A()
daxin.hello() # hello() = functools.partial(self.fn,owner)
类方法由于默认会把类当作参数传递,所以需要把方法的第一个参数固定为类,所以使用偏函数来固定,是一个比较好的办法,又或者使用lambda,由于lambda函数只能接受一个参数,所以当类方法是多个参数时,无法接受。
3.3 对实例的数据进行校验
现有如下代码:
class Person:
def __init__(self,name:str, age:int):
self.name = name
self.age = age
对上面类的属性name,age进行数据类型的校验。
思路:
- 写函数,直接在__init__中检查,如果不符合直接抛出异常(
一般人都会
) - 装饰器(
多数人会
) - 描述器(
少数人会
) - 描述器版装饰器(
基本没人会
)
3.3.1 直接在__init__函数中检查
class Person:
def __init__(self, name:str, age:int):
# 每次都判断,然后赋值
# if self._typecheck(name,str):
# self.name = name
# if self._typecheck(age, int):
# self.age = age
# 或者直接构建需要的数据类型,一次性判断,最后赋值
params = [(name,str),(age,int)]
for param in params:
if not self._typecheck(*param):
raise TypeError(param[0])
self.name = name
self.age = age
def _typecheck(self,value,typ):
if not isinstance(value, typ):
raise TypeError(value)
return True
daxin = Person('daxin',20)
print(daxin.name)
print(daxin.age)
看起来也太丑了,不能复用不说,在初始化阶段还做了大量的逻辑判断,也不容易让别人明白你真正的意图是啥。
3.3.2 装饰器版本
import inspect
def TypeCheck(cls:object):
def wrapper(*args,**kwargs):
sig = inspect.signature(cls) # 获取签名对象
param = sig.parameters.values() # 抽取签名信息(有序)
data = zip(args,param) # 构建值与类型的元组
for value,typ in data:
if typ.annotation != inspect._empty: # 当定义了参数注解时,开始参数判断
if not isinstance(value,typ.annotation):
raise TypeError(value) # 判断不通过,爆出异常
return cls(*args,**kwargs)
return wrapper
@TypeCheck # Person = TypeCheck(Person)('daxin',20) ==> wrapper('daxin',20)
class Person:
def __init__(self,name:str, age:int):
self.name = name
self.age = age
daxin = Person('daxin','20')
print(daxin.name)
print(daxin.age)
看起来很好的解决了参数类型的检查,并且也可以针对不同类继续进行参数检查,所以说:装饰器
,真香
。
3.3.3 描述器版本
class TypeCheck:
def __init__(self, name, typ):
self.name = name
self.typ = typ
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value,self.typ):
raise TypeError(value)
instance.__dict__[self.name] = value
class Person:
name = TypeCheck('name',str) # 硬编码
age = TypeCheck('age',int) # 硬编码
def __init__(self, name:str, age:int):
self.name = name
self.age = age
daxin = Person('daxin','20')
print(daxin.name)
print(daxin.age)
3.3.4 装饰器+描述器版本之函数装饰器
import inspect
class TypeCheck:
def __init__(self, name, typ):
self.name = name
self.typ = typ
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value,self.typ):
raise TypeError(value)
instance.__dict__[self.name] = value
# 动态注入name,age描述器属性
def AttriCheck(cls:object):
def wrapper(*args,**kwargs):
sig = inspect.signature(cls)
params = sig.parameters
for k,v in params.items():
print(v.annotation)
if v.annotation != inspect._empty:
if not hasattr(cls,k):
setattr(cls,k,TypeCheck(k,v.annotation))
return cls(*args,**kwargs)
return wrapper
@AttriCheck # Person = AttriCheck(Person)
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
a = Person('daxin', 20)
print(a.name)
print(a.age)
使用装饰器结合描述器时,类必须包含对应同名描述器,才可以利用描述器进行参数检查,所以,利用反射,将参数注入类中,然后通过描述器进行检查
3.3.5 装饰器+描述器版本之类装饰器
能否把上面的装饰器函数,改为类?
import inspect
class TypeCheck:
def __init__(self, name, typ):
self.name = name
self.typ = typ
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value,self.typ):
raise TypeError(value)
instance.__dict__[self.name] = value
class AttriCheck:
def __init__(self,cls):
self.cls = cls
def __call__(self, *args, **kwargs):
sig = inspect.signature(self.cls)
params = sig.parameters
for name,typ in params.items():
if typ.annotation != inspect._empty:
if not hasattr(self.cls, name):
setattr(self.cls,name,TypeCheck(name,typ.annotation))
return self.cls(*args,**kwargs)
@AttriCheck # Person = AttriCheck(Person)
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
a = Person('daxin', '20')
print(a.name)
print(a.age)
4 疑问
看下面例子:
class B:
def __init__(self, data):
self.data = data
def __get__(self, instance, owner):
return self.data
def __set__(self, instance, value):
self.data = value
class C:
name = B('daxin')
age = B(20)
def __init__(self, name, age):
self.name = name
self.age = age
daxin = C('tom',18)
dachenzi = C('Jack',29)
print(daxin.name)
结果是'Jack',为什么呢?
- name和age属于类属性,只会在定义的时候实例化一次!不同实例的name和age属性是公用的!
- 在描述器中,把实例设置的值,绑定到了描述器本身的属性上去了。
- 不同实例的name和age属性都指向了相同的描述器,并且每次修改的都是同一个属性。
- 这种坑是要避免的,尽量把属性绑定在实例自己身上