起因
在公众号里读到了这篇文章《这个 Python 知识点,90% 的人都得挂~》,一直读到第三点 基于描述符如何实现property
的时候懵了,于是去翻文档重新学习。(公众号的文章里叫描述符,翻译的文档叫描述器,以下统一称Descriptor为描述器)
学!
什么是描述器
descriptor 就是任何一个定义了 __get__()
,__set__()
或 __delete__()
的对象。
可选地,描述器可以具有 __set_name__()
方法。这仅在描述器需要知道创建它的类或分配给它的类变量名称时使用。(即使该类不是描述器,只要此方法存在就会调用。)
描述器的四个方法
set_name
这个函数甚至在公众号的文章里没有被介绍(不然最后那部分的代码可以简洁很多),它是3.6版本的新功能,一般定义如下:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
其中,self
代表着描述器本身,owner
代表了创建它的类, name
代表了分配给它的类变量名。
举个例子:
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
...
假设这里的String
是一个描述器,那么上述的self owner name
就分别是name(String的一个实例) Component类 字符串‘name’
。那么有可能有人就要问了,到底有啥用呢?这里就要提到描述器的一种流行用法,就是托管对实例数据的访问,描述器被分配给类字典中的公开属性,而实际数据作为私有属性存储在实例字典中。
在上面一个代码块里,我们实现了描述器被分配给类字典中的公开属性这第一步,那么第二步我们先通过这个函数得到了对应的私有属性名,即self.private_name
,数据怎么储存就看下面的函数啦。
set
先给出标准的定义:
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
这里obj
就是绑定了描述符的实例,通过setattr
直接把实际数据储存在实例的字典里。
get
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
return value
如何拿到对应的数据,这里的objtype
是obj
这个实例的类型。
如果不是通过实例(a.x)而是通过类来访问(A.x),会直接调用desc.__get__(None, A)
,因此有些会加个判断:
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = getattr(obj, self.private_name)
return value
补充
可以看到上面函数里面,像__set_name__
的参数owner
和__get__
的参数objtype
你怎么没有用到呀。因为上面介绍用的例子都太简单了,甚至可以 说是没啥用。
想这个用描述器来实现简单ORM框架的例子,就通过owner
拿到了对应类的类属性。
属性的访问
如果一个对象定义了 __set__()
或 __delete__()
,则它会被视为数据描述器。 仅定义了 __get__()
的描述器称为非数据描述器(它们经常被用于方法,但也可以有其他用途)。
属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。对于实例来说,a.x 的查找顺序会从 a.__dict__[‘x’] 开始,然后是 type(a).__dict__[‘x’],接下来依次查找 type(a)
的方法解析顺序(MRO
)。 如果找到的值是定义了某个描述器方法的对象,则 Python
可能会重写默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。
点运算符的查找逻辑在 object.__getattribute__()
中。这里是一个等价的纯 Python
实现:
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
可以看到数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__()
(如果存在的话)。
如果是描述器的话,会直接调用描述器的__get__
去拿数据。
Property的实现(解惑)
定义:
class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
调用:
class Student:
def __init__(self, name):
self.name = name
# 其实只有这里改变
@Property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
我们看到第一个装饰器,等价于math = Property(fget=math, fset=None, fdel=None, doc=None)
,于是math
变成了一个描述器的实例,对于整个Student
类来说,类变量math
绑定了一个描述器,正好符合描述器的使用场景。
接着看第二个装饰器,等价于math = math.setter(math)
,等价于math = Property(fget=math, fset=math, None, None)
,看着可能有点晕,参数里的第一个math
是上面装饰器装饰的函数,储存在第一个实例的self.get
里面,作为参数传入到第二个实例的构造函数里面,第二个math
就是第二个装饰器装饰的函数啦。
想通了应该还是比较好理解的,总结一下就是把类变量math
绑定到了一个描述器上,第一个函数作为fget
传入,第二个函数作为fset
传入。但我们访问或改变Student
实例的math
属性时,就会调用描述器里的__get__
和__set__
函数,而它们又会分别调用我们刚刚用装饰器装饰的两个函数,感觉是绕了一圈呀。
应用
描述器的使用贯穿了整个语言。就是它让函数变成绑定方法。常见工具诸如 classmethod()
, staticmethod()
,property()
和 functools.cached_property()
都作为描述器实现。
纯 Python 版本的实现在文档里都有,但我觉得最秀的还是验证器和ORM啦。