描述符指南¶
- 作者:
Raymond Hettinger
- 联系:
<python at rcn dot com>
描述符 允许对象自定义属性查找、存储和删除。
本指南包含四个主要部分
“入门”部分提供基本概述,从简单的示例开始,逐步添加功能。如果您是描述符新手,请从这里开始。
第二部分展示了一个完整的实用描述符示例。如果您已经了解基础知识,请从这里开始。
第三部分提供更技术性的教程,深入介绍描述符的工作机制。大多数人不需要了解这些细节。
最后一部分包含用纯 Python 编写的内置描述符的等效项,这些描述符是用 C 编写的。如果您想了解函数如何变成绑定方法,或者想了解
classmethod()
、staticmethod()
、property()
和 __slots__ 等常用工具的实现,请阅读本部分。
入门¶
在本入门部分,我们将从最基本的示例开始,然后逐步添加新功能。
简单示例:返回常量的描述符¶
Ten
类是一个描述符,其 __get__()
方法始终返回常量 10
class Ten:
def __get__(self, obj, objtype=None):
return 10
要使用描述符,必须将其存储为另一个类的类变量
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
交互式会话展示了普通属性查找和描述符查找之间的区别
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
在 a.x
属性查找中,点运算符在类字典中找到 'x': 5
。在 a.y
查找中,点运算符找到一个描述符实例,通过其 __get__
方法识别。调用该方法返回 10
。
请注意,值 10
既没有存储在类字典中,也没有存储在实例字典中。相反,值 10
是按需计算的。
此示例展示了简单描述符的工作原理,但它并不实用。对于检索常量,普通属性查找会更好。
在下一节中,我们将创建一个更有用的东西,动态查找。
动态查找¶
有趣的描述符通常运行计算而不是返回常量
import os
class DirectorySize:
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # Descriptor instance
def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute
交互式会话表明查找是动态的——它每次都会计算不同的、更新的答案
>>> 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
除了展示描述符如何运行计算之外,此示例还揭示了参数的用途 __get__()
。self 参数是 size,一个 DirectorySize 的实例。obj 参数是 g 或 s,一个 Directory 的实例。正是 obj 参数让 __get__()
方法了解目标目录。objtype 参数是类 Directory。
托管属性¶
描述符的常见用途是管理对实例数据的访问。描述符被分配给类字典中的公共属性,而实际数据存储在实例字典中的私有属性中。描述符的 __get__()
和 __set__()
方法在访问公共属性时被触发。
在以下示例中,age 是公共属性,_age 是私有属性。当访问公共属性时,描述符会记录查找或更新
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
def __get__(self, obj, objtype=None):
value = obj._age
logging.info('Accessing %r giving %r', 'age', value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', 'age', value)
obj._age = value
class Person:
age = LoggedAgeAccess() # Descriptor instance
def __init__(self, name, age):
self.name = name # Regular instance attribute
self.age = age # Calls __set__()
def birthday(self):
self.age += 1 # Calls both __get__() and __set__()
交互式会话表明对托管属性 age 的所有访问都被记录,但常规属性 name 没有被记录
>>> 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
>>> vars(mary) # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}
>>> mary.age # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday() # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name # Regular attribute lookup isn't logged
'David D'
>>> dave.age # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40
此示例的一个主要问题是私有名称 _age 在 LoggedAgeAccess 类中是硬编码的。这意味着每个实例只能有一个记录的属性,并且它的名称是不可更改的。在下一个示例中,我们将解决这个问题。
自定义名称¶
当一个类使用描述符时,它可以通知每个描述符使用了哪个变量名。
在此示例中,Person
类有两个描述符实例,name 和 age。当 Person
类被定义时,它会回调 __set_name__()
在 LoggedAccess 中,以便可以记录字段名称,为每个描述符提供自己的 public_name 和 private_name
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
logging.info('Accessing %r giving %r', self.public_name, value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', self.public_name, value)
setattr(obj, self.private_name, value)
class Person:
name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age):
self.name = name # Calls the first descriptor
self.age = age # Calls the second descriptor
def birthday(self):
self.age += 1
交互式会话表明 Person
类已调用 __set_name__()
以便记录字段名称。在这里,我们调用 vars()
来查找描述符,而不会触发它
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
新类现在记录对 name 和 age 的访问
>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20
两个 Person 实例只包含私有名称
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
结束语¶
一个 描述符 是我们对任何定义 __get__()
、__set__()
或 __delete__()
的对象的称呼。
可选地,描述符可以有一个 __set_name__()
方法。这仅在描述符需要知道创建它的类或分配给它的类变量的名称的情况下使用。(如果存在,即使类不是描述符,也会调用此方法。)
描述符在属性查找期间通过点运算符被调用。如果描述符通过 vars(some_class)[descriptor_name]
间接访问,则会返回描述符实例,而不会调用它。
描述符仅在用作类变量时才有效。当放入实例中时,它们没有任何效果。
描述符的主要动机是提供一个钩子,允许存储在类变量中的对象控制属性查找期间发生的事情。
传统上,调用类控制查找期间发生的事情。描述符颠倒了这种关系,并允许被查找的数据对此事有发言权。
描述符在整个语言中使用。这就是函数如何变成绑定方法的方式。像 classmethod()
、staticmethod()
、property()
和 functools.cached_property()
这样的常用工具都是用描述符实现的。
完整的实际示例¶
在这个例子中,我们创建了一个实用且强大的工具来定位臭名昭著的难以找到的数据损坏错误。
验证器类¶
验证器是用于管理属性访问的描述符。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果这些限制没有得到满足,它会引发异常以防止数据在其源头被破坏。
这个 Validator
类既是 抽象基类 也是一个管理的属性描述符
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
自定义验证器需要继承自 Validator
并且必须提供一个 validate()
方法来根据需要测试各种限制。
自定义验证器¶
这里有三个实用的数据验证实用程序
OneOf
验证一个值是否是一组受限选项之一。
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
实际应用¶
以下是数据验证器如何在真实类中使用的方法
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
描述符防止创建无效的实例
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
Traceback (most recent call last):
...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
Traceback (most recent call last):
...
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid
技术教程¶
以下是关于描述符工作机制和细节的更技术性的教程。
抽象¶
定义描述符,总结协议,并展示如何调用描述符。提供一个示例,展示对象关系映射的工作原理。
了解描述符不仅可以访问更大的工具集,还可以更深入地了解 Python 的工作原理。
定义和介绍¶
一般来说,描述符是一个属性值,它具有描述符协议中的一个方法。这些方法是 __get__()
、__set__()
和 __delete__()
。如果为某个属性定义了任何这些方法,则称该属性为 描述符。
属性访问的默认行为是从对象的字典中获取、设置或删除属性。例如,a.x
的查找链从 a.__dict__['x']
开始,然后是 type(a).__dict__['x']
,并继续遍历 type(a)
的方法解析顺序。如果查找的值是定义了描述符方法之一的对象,那么 Python 可能会覆盖默认行为,并改为调用描述符方法。此行为在优先级链中的位置取决于定义了哪些描述符方法。
描述符是一种功能强大、通用的协议。它们是属性、方法、静态方法、类方法和 super()
背后的机制。它们在整个 Python 本身中使用。描述符简化了底层的 C 代码,并为日常 Python 程序提供了一套灵活的新工具。
描述符协议¶
descr.__get__(self, obj, type=None)
descr.__set__(self, obj, value)
descr.__delete__(self, obj)
这就是全部内容。定义任何这些方法,对象就被认为是描述符,并且可以在作为属性查找时覆盖默认行为。
如果对象定义了 __set__()
或 __delete__()
,则它被认为是数据描述符。仅定义 __get__()
的描述符称为非数据描述符(它们通常用于方法,但也可以用于其他用途)。
数据描述符和非数据描述符在计算覆盖时,相对于实例字典中的条目有所不同。如果实例字典中有一个与数据描述符同名的条目,则数据描述符优先。如果实例字典中有一个与非数据描述符同名的条目,则字典条目优先。
要创建一个只读数据描述符,请定义 __get__()
和 __set__()
,并让 __set__()
在被调用时引发 AttributeError
。用一个引发异常的占位符定义 __set__()
方法足以使其成为数据描述符。
描述符调用的概述¶
描述符可以直接使用 desc.__get__(obj)
或 desc.__get__(None, cls)
调用。
但描述符更常见的是从属性访问自动调用。
表达式 obj.x
在 obj
的命名空间链中查找属性 x
。如果搜索在实例 __dict__
之外找到描述符,则根据下面列出的优先级规则调用其 __get__()
方法。
调用的细节取决于 obj
是一个对象、类还是 super 的实例。
从实例调用¶
实例查找扫描命名空间链,赋予数据描述符最高优先级,其次是实例变量,然后是非数据描述符,然后是类变量,最后是 __getattr__()
(如果提供)。
如果为 a.x
找到描述符,则使用以下方式调用它:desc.__get__(a, type(a))
。
点式查找的逻辑在 object.__getattribute__()
中。以下是一个纯 Python 等效项
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(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)
注意,__getattribute__()
代码中没有 __getattr__()
钩子。这就是为什么直接调用 __getattribute__()
或使用 super().__getattribute__
会完全绕过 __getattr__()
。
相反,是点运算符和 getattr()
函数负责在 __getattribute__()
抛出 AttributeError
时调用 __getattr__()
。它们的逻辑封装在一个辅助函数中
def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__
从类调用¶
点式查找(如 A.x
)的逻辑在 type.__getattribute__()
中。步骤类似于 object.__getattribute__()
,但实例字典查找被对类 方法解析顺序 的搜索所取代。
如果找到描述符,则使用 desc.__get__(None, A)
调用它。
完整的 C 实现可以在 Objects/typeobject.c 中的 type_getattro()
和 _PyType_Lookup()
中找到。
从 super 调用¶
super 的点式查找的逻辑在 super()
返回的对象的 __getattribute__()
方法中。
点式查找(如 super(A, obj).m
)在 obj.__class__.__mro__
中搜索紧随 A
之后的基类 B
,然后返回 B.__dict__['m'].__get__(obj, A)
。如果不是描述符,则返回 m
不变。
完整的 C 实现可以在 super_getattro()
中找到,位于 Objects/typeobject.c。纯 Python 等效代码可以在 Guido 的教程 中找到。
调用逻辑概述¶
描述符机制嵌入在 object
、type
和 super()
的 __getattribute__()
方法中。
需要记住的重要要点是
描述符由
__getattribute__()
方法调用。覆盖
__getattribute__()
会阻止自动描述符调用,因为所有描述符逻辑都在该方法中。object.__getattribute__()
和type.__getattribute__()
对__get__()
进行不同的调用。第一个包括实例,可能包括类。第二个将None
作为实例,并且始终包括类。数据描述符始终覆盖实例字典。
非数据描述符可以被实例字典覆盖。
自动名称通知¶
有时描述符需要知道它被分配到的类变量名称。当创建新类时,type
元类会扫描新类的字典。如果任何条目是描述符,并且它们定义了 __set_name__()
,则会使用两个参数调用该方法。owner 是使用描述符的类,name 是描述符被分配到的类变量。
实现细节在 Objects/typeobject.c 中的 type_new()
和 set_names()
中。
由于更新逻辑在 type.__new__()
中,因此通知仅在类创建时发生。如果之后将描述符添加到类中,则需要手动调用 __set_name__()
。
ORM 示例¶
以下代码是一个简化的骨架,展示了如何使用数据描述符来实现 对象关系映射。
基本思想是数据存储在外部数据库中。Python 实例只保存数据库表的键。描述符负责查找或更新。
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
我们可以使用 Field
类来定义 模型,这些模型描述了数据库中每个表的模式。
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
def __init__(self, key):
self.key = key
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
要使用这些模型,首先连接到数据库。
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
交互式会话展示了如何从数据库中检索数据以及如何更新数据。
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'
纯 Python 等价物¶
描述符协议很简单,并提供了令人兴奋的可能性。一些用例非常常见,以至于它们已被预先打包到内置工具中。属性、绑定方法、静态方法、类方法和 __slots__ 都是基于描述符协议的。
属性¶
调用 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.")
要了解 property()
如何根据描述符协议实现,以下是一个纯 Python 等价物
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
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no getter'
)
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no setter'
)
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no deleter'
)
self.fdel(obj)
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
当用户界面授予属性访问权限,而后续更改需要方法干预时,内置的 property()
会有所帮助。
例如,一个电子表格类可以通过 Cell('b10').value
授予对单元格值的访问权限。程序的后续改进要求单元格在每次访问时都重新计算;但是,程序员不希望影响直接访问属性的现有客户端代码。解决方案是将对 value 属性的访问包装在一个属性数据描述符中。
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
内置的 property()
或我们的 Property()
等价物都可以在此示例中使用。
函数和方法¶
Python 的面向对象特性建立在基于函数的环境之上。使用非数据描述符,两者可以无缝地合并。
存储在类字典中的函数在调用时会变成方法。方法与普通函数的区别仅在于对象实例被追加到其他参数之前。按照惯例,实例被称为 self,但可以被称为 this 或任何其他变量名。
可以使用 types.MethodType
手动创建方法,它大致等同于
class MethodType:
"Emulate PyMethod_Type in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
为了支持自动创建方法,函数包含 __get__()
方法,用于在属性访问期间绑定方法。这意味着函数是非数据描述符,在从实例进行点式查找时会返回绑定方法。以下是它的工作原理
class Function:
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return MethodType(self, obj)
在解释器中运行以下类,展示了函数描述符在实践中的工作原理
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 0x00B18C90>
如果你曾经想知道在普通方法中self来自哪里,或者在类方法中cls来自哪里,这就是答案!
方法类型¶
非数据描述符提供了一种简单的机制,用于在将函数绑定到方法的常用模式上进行变体。
回顾一下,函数有一个__get__()
方法,以便它们可以在被访问为属性时转换为方法。非数据描述符将obj.f(*args)
调用转换为f(obj, *args)
。调用cls.f(*args)
变为f(*args)
。
此图表总结了绑定及其两个最常用的变体
转换
从对象调用
从类调用
函数
f(obj, *args)
f(*args)
staticmethod
f(*args)
f(*args)
classmethod
f(type(obj), *args)
f(cls, *args)
静态方法¶
静态方法返回底层函数,没有任何更改。调用c.f
或C.f
等效于直接查找object.__getattribute__(c, "f")
或object.__getattribute__(C, "f")
。因此,该函数可以从对象或类中以相同的方式访问。
静态方法的良好候选者是不引用self
变量的方法。
例如,一个统计包可能包含一个用于实验数据的容器类。该类提供了用于计算平均值、均值、中位数和其他依赖于数据的描述性统计信息的普通方法。但是,可能存在概念上相关但并不依赖于数据的有用函数。例如,erf(x)
是一个方便的转换例程,它出现在统计工作中,但并不直接依赖于特定数据集。它可以从对象或类中调用:s.erf(1.5) --> .9332
或Sample.erf(1.5) --> .9332
。
由于静态方法返回底层函数,没有任何更改,因此示例调用并不令人兴奋
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
使用非数据描述符协议,staticmethod()
的纯 Python 版本将如下所示
import functools
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
functools.update_wrapper()
调用添加了一个__wrapped__
属性,该属性引用底层函数。此外,它还传递了使包装器看起来像被包装函数的属性:__name__
、__qualname__
、__doc__
和__annotations__
。
类方法¶
与静态方法不同,类方法在调用函数之前将类引用添加到参数列表的开头。这种格式对于调用者是对象还是类来说都是一样的
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
这种行为在方法只需要类引用并且不依赖于特定实例中存储的数据时很有用。类方法的一种用途是创建备用类构造函数。例如,类方法dict.fromkeys()
从键列表创建一个新的字典。纯 Python 等效项是
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}
使用非数据描述符协议,classmethod()
的纯 Python 版本看起来像这样
import functools
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
hasattr(type(self.f), '__get__')
的代码路径是在 Python 3.9 中添加的,它使 classmethod()
可以支持链式装饰器。例如,可以将类方法和属性链接在一起。在 Python 3.11 中,此功能已弃用。
class G:
@classmethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"
ClassMethod
中的 functools.update_wrapper()
调用添加了一个 __wrapped__
属性,该属性引用底层函数。此外,它还传递了使包装器看起来像被包装函数所需的属性:__name__
、__qualname__
、__doc__
和 __annotations__
。
成员对象和 __slots__¶
当一个类定义 __slots__
时,它会用固定长度的插槽值数组替换实例字典。从用户的角度来看,这有几个影响
1. 提供对由于拼写错误的属性赋值而导致的错误的立即检测。只允许在 __slots__
中指定的属性名称
class Vehicle:
__slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
2. 有助于创建不可变对象,其中描述符管理对存储在 __slots__
中的私有属性的访问
class Immutable:
__slots__ = ('_dept', '_name') # Replace the instance dictionary
def __init__(self, dept, name):
self._dept = dept # Store to private attribute
self._name = name # Store to private attribute
@property # Read-only descriptor
def dept(self):
return self._dept
@property
def name(self): # Read-only descriptor
return self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
...
AttributeError: property 'dept' of 'Immutable' object has no setter
>>> mark.location = 'Mars'
Traceback (most recent call last):
...
AttributeError: 'Immutable' object has no attribute 'location'
3. 节省内存。在 64 位 Linux 构建中,具有两个属性的实例在使用 __slots__
时占用 48 字节,而不使用时占用 152 字节。这种 享元设计模式 只有在要创建大量实例时才重要。
4. 提高速度。使用 __slots__
读取实例变量的速度提高了 35%(在 Apple M1 处理器上使用 Python 3.10 测量)。
5. 阻止像 functools.cached_property()
这样的工具,这些工具需要实例字典才能正常工作
from functools import cached_property
class CP:
__slots__ = () # Eliminates the instance dict
@cached_property # Requires an instance dict
def pi(self):
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
for n in reversed(range(100_000)))
>>> CP().pi
Traceback (most recent call last):
...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
无法创建 __slots__
的精确的纯 Python 版本,因为它需要直接访问 C 结构并控制对象内存分配。但是,我们可以构建一个几乎完全忠实的模拟,其中插槽的实际 C 结构由私有的 _slotvalues
列表模拟。对该私有结构的读写由成员描述符管理
null = object()
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/structmember.h'
# Also see descr_new() in Objects/descrobject.c
self.name = name
self.clsname = clsname
self.offset = offset
def __get__(self, obj, objtype=None):
'Emulate member_get() in Objects/descrobject.c'
# Also see PyMember_GetOne() in Python/structmember.c
if obj is None:
return self
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
return value
def __set__(self, obj, value):
'Emulate member_set() in Objects/descrobject.c'
obj._slotvalues[self.offset] = value
def __delete__(self, obj):
'Emulate member_delete() in Objects/descrobject.c'
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
obj._slotvalues[self.offset] = null
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'
def __new__(mcls, clsname, bases, mapping, **kwargs):
'Emulate type_new() in Objects/typeobject.c'
# type_new() calls PyTypeReady() which calls add_methods()
slot_names = mapping.get('slot_names', [])
for offset, name in enumerate(slot_names):
mapping[name] = Member(name, clsname, offset)
return type.__new__(mcls, clsname, bases, mapping, **kwargs)
object.__new__()
方法负责创建具有插槽而不是实例字典的实例。以下是纯 Python 中的一个粗略模拟
class Object:
'Simulate how object.__new__() allocates memory for __slots__'
def __new__(cls, *args, **kwargs):
'Emulate object_new() in Objects/typeobject.c'
inst = super().__new__(cls)
if hasattr(cls, 'slot_names'):
empty_slots = [null] * len(cls.slot_names)
object.__setattr__(inst, '_slotvalues', empty_slots)
return inst
def __setattr__(self, name, value):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__setattr__(name, value)
def __delattr__(self, name):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__delattr__(name)
要在实际类中使用模拟,只需从 Object
继承并设置 元类 为 Type
class H(Object, metaclass=Type):
'Instance variables stored in slots'
slot_names = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
此时,元类已为 x 和 y 加载了成员对象
>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
'__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'],
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
'x': <Member 'x' of 'H'>,
'y': <Member 'y' of 'H'>}
创建实例时,它们有一个 slot_values
列表,用于存储属性
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
拼写错误或未分配的属性将引发异常
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'