描述符操作指南 — Python 文档

来自菜鸟教程
Python/docs/3.10/howto/descriptor
跳转至:导航、​搜索

描述符操作指南

作者
雷蒙德·赫廷格
接触

Descriptors 让对象自定义属性查找、存储和删除。

本指南有四个主要部分:

  1. “入门”给出了一个基本的概述,从简单的例子开始,一次添加一个功能。 如果您不熟悉描述符,请从这里开始。
  2. 第二部分显示了一个完整的、实用的描述符示例。 如果您已经了解基础知识,请从那里开始。
  3. 第三部分提供了一个更具技术性的教程,其中详细介绍了描述符的工作原理。 大多数人不需要这种详细程度。
  4. 最后一部分是用 C 编写的内置描述符的纯 Python 等效项。 如果您对函数如何变成绑定方法或 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参数是sizeDirectorySize的一个实例。 obj 参数是 gsDirectory 的一个实例。 正是 obj 参数让 [X47X] 方法学习目标目录。 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 类有两个描述符实例,nameage。 当定义了 Person 类时,它会回调 LoggedAccess 中的 __set_name__() 以便记录字段名称,为每个描述符赋予自己的 public_name[ X182X] 和 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'}

新类现在记录对 nameage 的访问:

>>> 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() 方法来根据需要测试各种限制。


自定义验证器

以下是三个实用的数据验证实用程序:

  1. OneOf 验证一个值是一组受限制的选项之一。
  2. Number 验证值是 intfloat。 可选地,它验证一个值是否在给定的最小值或最大值之间。
  3. String 验证一个值是一个 str。 或者,它验证给定的最小或最大长度。 它也可以验证用户定义的 谓词
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) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

这就是它的全部内容。 定义这些方法中的任何一个,并且对象被视为描述符,并且可以在作为属性查找时覆盖默认行为。

如果一个对象定义了 __set__()__delete__(),它被认为是一个数据描述符。 仅定义 __get__() 的描述符称为非数据描述符(它们通常用于方法,但也可以用于其他用途)。

数据和非数据描述符的不同之处在于如何针对实例字典中的条目计算覆盖。 如果实例的字典有一个与数据描述符同名的条目,则数据描述符优先。 如果实例的字典具有与非数据描述符同名的条目,则字典条目优先。

要创建只读数据描述符,请同时定义 __get__()__set__(),其中 __set__() 在调用时引发 AttributeError。 使用异常引发占位符定义 __set__() 方法足以使其成为数据描述符。


描述符调用概述

可以直接使用 desc.__get__(obj)desc.__get__(None, cls) 调用描述符。

但更常见的是从属性访问中自动调用描述符。

表达式 obj.xobj 的命名空间链中查找属性 x。 如果搜索找到实例 __dict__ 之外的描述符,则根据下面列出的优先级规则调用其 __get__() 方法。

调用的细节取决于 obj 是 super 的对象、类还是实例。


从实例调用

实例查找扫描一系列命名空间,赋予数据描述符最高优先级,然后是实例变量,然后是非数据描述符,然后是类变量,最后是 __getattr__()(如果提供)。

如果找到 a.x 的描述符,则调用它:desc.__get__(a, type(a))

虚线查找的逻辑在 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)

有趣的是,属性查找并没有直接调用 object.__getattribute__()。 相反,点运算符和 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__

因此,如果 __getattr__() 存在,则每当 __getattribute__() 引发 AttributeError(直接或在描述符调用之一中)时都会调用它。

此外,如果用户直接调用 object.__getattribute__(),则完全绕过 __getattr__() 钩子。


从类调用

诸如 A.x 之类的点查找的逻辑在 type.__getattribute__() 中。 这些步骤与 object.__getattribute__() 的步骤类似,但实例字典查找被替换为通过类的 方法解析顺序 的搜索。

如果找到描述符,则使用 desc.__get__(None, A) 调用它。

完整的 C 实现可以在 :source:`Objects/typeobject.c`type_getattro()_PyType_Lookup() 中找到。


来自 super 的调用

super的点分查找逻辑在super()返回的对象的__getattribute__()方法中。

诸如 super(A, obj).m 之类的点分查找在 A 之后立即搜索基类 Bobj.__class__.__mro__,然后返回 B.__dict__['m'].__get__(obj, A)。 如果不是描述符,则返回 m 不变。

完整的 C 实现可以在 :source:`Objects/typeobject.c`super_getattro() 中找到。 可以在 Guido 的教程 中找到纯 Python 等价物。


调用逻辑总结

描述符的机制嵌入在 objecttypesuper()__getattribute__() 方法中。

要记住的要点是:

  • 描述符由 __getattribute__() 方法调用。
  • 类从 objecttypesuper() 继承这个机制。
  • 覆盖 __getattribute__() 可防止自动描述符调用,因为所有描述符逻辑都在该方法中。
  • object.__getattribute__()type.__getattribute__()__get__() 进行不同的调用。 第一个包括实例并且可能包括类。 第二个为实例输入 None 并始终包含类。
  • 数据描述符总是覆盖实例字典。
  • 非数据描述符可能会被实例字典覆盖。


自动姓名通知

有时希望描述符知道它被分配给什么类变量名。 创建新类时,type 元类会扫描新类的字典。 如果任何条目是描述符并且如果它们定义了 __set_name__(),则该方法将使用两个参数调用。 owner 是使用描述符的类,name 是描述符分配给的类变量。

实现细节在 :source:`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 类来定义 models 来描述数据库中每个表的模式:

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'unreadable attribute {self._name}')
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError(f"can't set attribute {self._name}")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError(f"can't delete attribute {self._name}")
        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 0x1012e1f98>

如果您曾经想知道 self 在常规方法中来自何处,或者在类方法中 cls 来自何处,就是这样!


方法种类

非数据描述符为将函数绑定到方法的常用模式的变化提供了一种简单的机制。

回顾一下,函数有一个 __get__() 方法,以便在作为属性访问时可以将它们转换为方法。 非数据描述符将 obj.f(*args) 调用转换为 f(obj, *args)。 调用 cls.f(*args) 变为 f(*args)

此图表总结了绑定及其两个最有用的变体:

转型 从对象调用 从班级调用
功能 f(obj, *args) f(*参数)
静态方法 f(*参数) f(*参数)
类方法 f(type(obj), *args) f(cls, *args)


静态方法

静态方法原样返回底层函数。 调用 c.fC.f 相当于直接查找 object.__getattribute__(c, "f")object.__getattribute__(C, "f")。 结果,该函数变得可以从对象或类相同地访问。

静态方法的良好候选者是不引用 self 变量的方法。

例如,一个统计包可能包含一个用于实验数据的容器类。 该类提供了计算平均值、均值、中位数和其他依赖于数据的描述性统计量的常规方法。 但是,可能存在概念上相关但不依赖于数据的有用函数。 例如,erf(x) 是一种方便的转换例程,它出现在统计工作中,但不直接依赖于特定的数据集。 它可以从对象或类中调用:s.erf(1.5) --> .9332Sample.erf(1.5) --> .9332

由于静态方法返回未更改的底层函数,因此示例调用并不令人兴奋:

class E:
    @staticmethod
    def f(x):
        return x * 10
>>> E.f(3)
30
>>> E().f(3)
30

使用非数据描述符协议,staticmethod() 的纯 Python 版本将如下所示:

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

    def __call__(self, *args, **kwds):
        return self.f(*args, **kwds)

类方法

与静态方法不同,类方法在调用函数之前将类引用添加到参数列表中。 无论调用者是对象还是类,这种格式都是相同的:

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 版本将如下所示:

class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            return self.f.__get__(cls, cls)
        return MethodType(self.f, cls)

hasattr(type(self.f), '__get__') 的代码路径是在 Python 3.9 中添加的,使 classmethod() 支持链式装饰器成为可能。 例如,类方法和属性可以链接在一起:

class G:
    @classmethod
    @property
    def __doc__(cls):
        return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"

成员对象和 __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: can't set attribute
>>> mark.location = 'Mars'
Traceback (most recent call last):
    ...
AttributeError: 'Immutable' object has no attribute 'location'

3. 节省内存。 在 64 位 Linux 构建中,具有两个属性的实例占用 48 字节(带 __slots__)和 152 字节(不带)。 这种 享元设计模式 可能仅在要创建大量实例时才重要。

4. 提高速度。 读取实例变量是 35% faster 和 __slots__(在 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
        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):
        'Emuluate 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)

object.__new__() 方法负责创建具有插槽而不是实例字典的实例。 下面是一个纯 Python 的粗略模拟:

class Object:
    'Simulate how object.__new__() allocates memory for __slots__'

    def __new__(cls, *args):
        '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'{type(self).__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'{type(self).__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

此时,元类已经为xy加载了成员对象:

>>> 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'