Professional Documents
Culture Documents
发布 3.10.0
Contents
1 入门 3
1.1 简单示例:返回常量的描述器 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 动态查找 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 托管属性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 定制名称 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 结束语 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2 完整的实际例子 7
2.1 验证器类 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 自定义验证器 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.3 实际应用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3 技术教程 10
3.1 摘要 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.2 定义与介绍 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.3 描述器协议 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.4 描述器调用概述 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.5 通过实例调用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.6 通过类调用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.7 通过 super 调用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.8 调用逻辑总结 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.9 自动名称通知 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.10 ORM (对象关系映射)示例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
4 纯 Python 等价实现 13
4.1 属性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
4.2 函数和方法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.3 方法的种类 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.4 静态方法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.5 类方法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.6 成员对象和 __slots__ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1
作者 Raymond Hettinger(译者:wh2099 at outlook dot com)
联系方式 <python at rcn dot com>
目录
• 描述器使用指南
– 入门
* 简单示例:返回常量的描述器
* 动态查找
* 托管属性
* 定制名称
* 结束语
– 完整的实际例子
* 验证器类
* 自定义验证器
* 实际应用
– 技术教程
* 摘要
* 定义与介绍
* 描述器协议
* 描述器调用概述
* 通过实例调用
* 通过类调用
* 通过 super 调用
* 调用逻辑总结
* 自动名称通知
* ORM (对象关系映射)示例
– 纯 Python 等价实现
* 属性
* 函数和方法
* 方法的种类
* 静态方法
* 类方法
* 成员对象和 __slots__
描述器让对象能够自定义属性查找、存储和删除的操作。
本指南主要分为四个部分:
2
1)“入门”部分从简单的示例着手,逐步添加特性,从而给出基本的概述。如果你是刚接触到描述器,请
从这里开始。
2) 第二部分展示了完整的、实用的描述器示例。如果您已经掌握了基础知识,请从此处开始。
3) 第三部分提供了更多技术教程,详细介绍了描述器如何工作。大多数人并不需要深入到这种程度。
4) 最后一部分有对内置描述器(用 C 编写)的纯 Python 等价实现。如果您想了解函数如何变成绑定方法
或对 classmethod(),staticmethod(),property() 和 __slots__ 这类常见工具的实现感兴趣,
请阅读此部分。
1 入门
现在,让我们从最基本的示例开始,然后逐步添加新功能。
1.1 简单示例:返回常量的描述器
class Ten:
def __get__(self, obj, objtype=None):
return 10
要使用描述器,它必须作为一个类变量存储在另一个类中:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
用交互式会话查看普通属性查找和描述器查找之间的区别:
3
1.2 动态查找
有趣的描述器通常运行计算而不是返回常量:
import os
class DirectorySize:
class Directory:
交互式会话显示查找是动态的,每次都会计算不同的,经过更新的返回值:
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size # The songs directory has twenty files
20
>>> g.size # The games directory has three files
3
>>> os.remove('games/chess') # Delete a game
>>> g.size # File count is automatically updated
2
1.3 托管属性
描述器的一种流行用法是托管对实例数据的访问。描述器被分配给类字典中的公开属性,而实际数据作为私
有属性存储在实例字典中。当访问公开属性时,会触发描述器的 __get__() 和 __set__() 方法。
在下面的例子中,age 是公开属性,_age 是私有属性。当访问公开属性时,描述器会记录下查找或更新的日
志:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
4
(续上页)
class Person:
def birthday(self):
self.age += 1 # Calls both __get__() and __set__()
>>> mary = Person('Mary M', 30) # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
1.4 定制名称
当一个类使用描述器时,它可以告知每个描述器使用了什么变量名。
在此示例中,Person 类具有两个描述器实例 name 和 age。当类 Person 被定义的时候,他回调了 LoggedAccess
中的 __set_name__() 来记录字段名称,让每个描述器拥有自己的 public_name 和 private_name:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
5
(续上页)
self.private_name = '_' + name
class Person:
def birthday(self):
self.age += 1
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
6
1.5 结束语
2 完整的实际例子
在此示例中,我们创建了一个实用而强大的工具来查找难以发现的数据损坏错误。
2.1 验证器类
验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限
制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。
这个 Validator 类既是一个 abstract base class 也是一个托管属性描述器。
class Validator(ABC):
@abstractmethod
def validate(self, value):
pass
7
2.2 自定义验证器
这是三个实用的数据验证工具:
1) OneOf 验证值是一组受约束的选项之一。
2) Number 验证值是否为 int 或 float。根据可选参数,它还可以验证值在给定的最小值或最大值之间。
3) String 验证值是否为 str。根据可选参数,它可以验证给定的最小或最大长度。它还可以验证用户
定义的 predicate。
class OneOf(Validator):
class Number(Validator):
class String(Validator):
8
(续上页)
)
2.3 实际应用
这是在真实类中使用数据验证器的方法:
class Component:
描述器阻止无效实例的创建:
9
3 技术教程
接下来是专业性更强的技术教程,以及描述器工作原理的详细信息。
3.1 摘要
定义描述器,总结协议,并说明如何调用描述器。提供一个展示对象关系映射如何工作的示例。
学习描述器不仅能提供接触到更多工具集的途径,还能更深地理解 Python 工作的原理。
3.2 定义与介绍
3.3 描述器协议
3.4 描述器调用概述
10
3.5 通过实例调用
实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,
最后是 __getattr__() (如果存在的话)。
如果 a.x 找到了一个描述器,那么将通过 desc.__get__(a, type(a)) 调用它。
点运算符的查找逻辑在 object.__getattribute__() 中。这里是一个等价的纯 Python 实现:
3.6 通过类调用
11
3.7 通过 super 调用
3.8 调用逻辑总结
3.9 自动名称通知
class Field:
12
(续上页)
conn.execute(self.store, [value, obj.key])
conn.commit()
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
要使用模型,首先要连接到数据库:
交互式会话显示了如何从数据库中检索数据及如何对其进行更新:
4 纯 Python 等价实现
描述器协议很简单,但它提供了令人兴奋的可能性。有几个用例非常通用,以至于它们已预先打包到内置工
具中。属性、绑定方法、静态方法、类方法和 __slots__ 均基于描述器协议。
13
4.1 属性
调用 property() 是构建数据描述器的简洁方式,该数据描述器在访问属性时触发函数调用。它的签名是:
property(fget=None, fset=None, fdel=None, doc=None) -> property
该文档显示了定义托管属性 x 的典型用法:
class C:
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
(下页继续)
14
(续上页)
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
4.2 函数和方法
Python 的面向对象功能是在基于函数的环境构建的。通过使用非数据描述器,这两方面完成了无缝融合。
在调用时,存储在类词典中的函数将被转换为方法。方法与常规函数的不同之处仅在于对象实例被置于其他
参数之前。方法与常规函数的不同之处仅在于第一个参数是为对象实例保留的。按照惯例,实例引用称为
self ,但也可以称为 this 或任何其他变量名称。
可以使用 types.MethodType 手动创建方法,其行为基本等价于:
class MethodType:
"Emulate PyMethod_Type in Objects/classobject.c"
class Function:
...
15
在解释器中运行以下类,这显示了函数描述器的实际工作方式:
class D:
def f(self, x):
return x
>>> D.f.__qualname__
'D.f'
通过类字典访问函数不会调用 __get__()。相反,它只返回基础函数对象:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
来自类的点运算符访问会调用 __get__(),直接返回底层的函数。
>>> D.f
<function D.f at 0x00C45070>
有趣的行为发生在从实例进行点访问期间。点运算符查找调用 __get__(),返回绑定的方法对象:
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
绑定方法在内部存储了底层函数和绑定的实例:
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
4.3 方法的种类
非数据描述器为把函数绑定为方法的通常模式提供了一种简单的机制。
概括地说,函数对象具有 __get__() 方法,以便在作为属性访问时可以将其转换为方法。非数据描述器将
obj.f(*args) 的调用会被转换为 f(obj, *args) 。调用 klass.f(*args)‘ 因而变成 f(*args) 。
下表总结了绑定及其两个最有用的变体:
16
4.4 静态方法
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
4.5 类方法
与静态方法不同,类方法在调用函数之前将类引用放在参数列表的最前。无论调用方是对象还是类,此格式
相同:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
当方法仅需要具有类引用并且确实依赖于存储在特定实例中的数据时,此行为就很有用。类方法的一种用途
是创建备用类构造函数。例如,类方法 dict.fromkeys() 从键列表创建一个新字典。纯 Python 的等价实
现是:
17
class Dict(dict):
@classmethod
def fromkeys(cls, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c"
d = cls()
for key in iterable:
d[key] = value
return d
现在可以这样构造一个新的唯一键字典:
>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
class G:
@classmethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"
class Vehicle:
__slots__ = ('id_number', 'make', 'model')
18
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
2. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__:
class Immutable:
@property
def name(self): # Read-only descriptor
return self._name
3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152
bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.
4. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an
Apple M1 processor).
5. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:
class CP:
__slots__ = () # Eliminates the instance dict
>>> CP().pi
Traceback (most recent call last):
...
(下页继续)
19
(续上页)
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
null = object()
class Member:
def __repr__(self):
'Emulate member_repr() in Objects/descrobject.c'
return f'<Member {self.name!r} of {self.clsname!r}>'
type.__new__() 方法负责将成员对象添加到类变量:
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
class Object:
(下页继续)
20
(续上页)
'Simulate how object.__new__() allocates memory for __slots__'
21
(续上页)
>>> vars(h)
{'_slotvalues': [55, 20]}
错误拼写或未赋值的属性将引发一个异常:
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'
22