CPython 描述符机制详解:从 __get__ 协议到 property、classmethod 与 __slots__ 的底层实现
本文基于 CPython 官方文档 Descriptor Guide(Doc/howto/descriptor.rst)展开,系统讲解 Python 描述符(descriptor)协议:先用渐进式示例从"返回常量的描述符"一路推导到"带 __set_name__ 的可定制描述符",再给出一个可直接用于数据校验的实战工具;随后深入技术细节,用纯 Python 复刻 object.__getattribute__ 的查找逻辑,解释实例查找、类查找与 super() 查找中描述符的调用规则;最后拆解 property、函数到绑定方法的转换、staticmethod、classmethod 以及 __slots__ 的纯 Python 等价实现,并对照 CPython C 源码(Objects/typeobject.c、Objects/funcobject.c、Objects/descrobject.c)验证这些机制在解释器中的真实落点。读完后,你能够独立编写数据校验、访问日志、ORM 字段等描述符,并理解 self/cls 参数绑定的底层原理。
什么是描述符
描述符是任何定义了 __get__、__set__ 或 __delete__ 方法的对象。它让对象可以自定义属性的查找(lookup)、存储(storage)与删除(deletion)行为。描述符必须作为类变量存放在另一个类中才会生效;若放在实例里则不起作用。
理解描述符不仅扩展你的工具集,还能加深对 Python 运行机制的理解:属性、方法、静态方法、类方法、property 以及 super() 都是构建在描述符协议之上的。CPython 的 type_new_set_names()(见 Objects/typeobject.c)在类创建时扫描类字典,对每个定义了 __set_name__ 的条目自动回调——这就是官方文档所述"自动名称通知"的实现位置。
渐进式入门:从常量描述符到可定制名称
官方文档的入门部分从最简单的示例出发,逐个增加功能。
示例一:返回常量的描述符
最朴素的描述符只实现 __get__:
class Ten:
def __get__(self, obj, objtype=None):
return 10
class A:
x = 5 # 普通类属性
y = Ten() # 描述符实例
交互式会话中两者的区别一目了然:
>>> a = A()
>>> a.x # 普通属性查找:在类字典里找到 'x': 5
5
>>> a.y # 描述符查找:点号运算符发现 __get__,调用后返回 10
10
a.x 的查找过程是:点号运算符在类字典中找到 x: 5 并直接返回。a.y 的查找则是:点号运算符发现一个描述符实例(通过它的 __get__ 方法识别),调用该方法得到 10。
关键细节:值 10 既不存放在类字典里,也不存放在实例字典里,而是按需计算出来的。这个例子演示了机制但不实用——取常量用普通属性查找就够了。
示例二:动态查找
真正有意思的描述符通常执行计算而非返回常量:
import os
class DirectorySize:
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # 描述符实例
def __init__(self, dirname):
self.dirname = dirname # 普通实例属性
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size # songs 目录有 20 个文件
20
>>> g.size # games 目录有 3 个文件
3
>>> os.remove('games/chess') # 删除一个游戏
>>> g.size # 文件数自动更新
2
这个例子同时揭示了 __get__ 各参数的用途:
self是DirectorySize的实例(即size);obj是宿主类Directory的实例(s或g)——描述符正是靠obj得知目标目录的;objtype是宿主类Directory本身。
示例三:受管属性(日志访问)
描述符最常见的用途之一是管理对实例数据的访问:描述符作为公有属性挂在类字典中,真正的数据以私有属性形式存放在实例字典中;访问公有属性时触发 __get__ 和 __set__:
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() # 描述符实例
def __init__(self, name, age):
self.name = name # 普通实例属性
self.age = age # 触发 __set__()
def birthday(self):
self.age += 1 # 同时触发 __get__() 和 __set__()
>>> mary = Person('Mary M', 30) # 初始的年龄更新会被记录
INFO:root:Updating 'age' to 30
>>> vars(mary) # 真实数据存放在私有属性里
{'name': 'Mary M', '_age': 30}
>>> mary.age # 访问数据并记录查找日志
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday() # 更新同样被记录
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name # 普通属性查找不被记录
'David D'
这个例子的主要缺陷是:私有名 _age 被硬编码进了 LoggedAgeAccess 类——每个实例只能有一个被记录的属性,且名字不可更改。
示例四:借助 __set_name__ 自定义名称
当类使用描述符时,可以告知每个描述符它被赋给了哪个变量名。定义新类时,元类 type 会扫描类字典,对每个定义了 __set_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() # 第一个描述符实例
age = LoggedAccess() # 第二个描述符实例
def __init__(self, name, age):
self.name = name # 调用第一个描述符
self.age = age # 调用第二个描述符
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'}
>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
注意这里用 vars(vars(Person)['name']) 直接查看描述符实例本身而不触发它——这正是"描述符只有在点号查找时才会被调用"的体现。
对应到 CPython 源码,type_new_set_names()(Objects/typeobject.c)遍历新类型字典的每个条目,通过 _PyObject_LookupSpecial(value, &_Py_ID(__set_name__)) 检查是否定义了该特殊方法,若有则以 (type, key) 两个参数调用它。文档同时提醒:该通知逻辑位于 type.__new__ 中,只在类创建时执行一次;事后向类中添加描述符需要手动调用 __set_name__。
入门小结
- 描述符 = 定义了
__get__、__set__或__delete__的对象;可选地还可以有__set_name__(该方法即使类本身不是描述符也会被调用); - 描述符由点号运算符在属性查找过程中触发;用
vars(some_class)[descriptor_name]间接访问只会拿到描述符实例本身,不会触发它; - 描述符只在作为类变量时使用才生效,放在实例中无效;
- 核心动机:提供一种钩子,让存放在类变量中的对象能够控制属性查找时发生什么。传统上由"被调用的类"主导查找行为,描述符反转了这一关系,让被查找的数据本身拥有话语权;
- 描述符贯穿整个语言:函数变成绑定方法靠的是它,
classmethod、staticmethod、property、functools.cached_property等常用工具全部以描述符实现。
完整实战:数据校验描述符
官方文档给出了一个定位"难以发现的数据损坏 bug"的实战工具——校验器(validator):在存储任何数据之前先验证新值是否满足类型与取值范围约束,不满足就抛异常,从源头阻止数据损坏。
Validator 基类
Validator 既是一个抽象基类(ABC),又是一个受管属性描述符:
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:值必须属于某个受限选项集合;Number:值必须是int或float,可选地检查上下界;String:值必须是str,可选地检查最小/最大长度,并支持用户自定义谓词(predicate):
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 a 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) # 被拦截:'Widget' 不是全大写
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5) # 被拦截:'metle' 拼写错误
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5) # 被拦截:-5 是负数
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V') # 被拦截:'V' 不是数字
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5) # 放行:输入全部合法
这套模式可以直接迁移到真实项目:字段约束、访问审计、延迟加载都能用同样的 __set_name__ + __get__/__set__ 骨架实现。
技术原理:描述符协议与调用机制
描述符协议的定义
descr.__get__(self, obj, type=None)
descr.__set__(self, obj, value)
descr.__delete__(self, obj)
仅此而已。定义了其中任何一个方法,对象就视为描述符,能在被作为属性查找时覆盖默认行为。默认行为是把属性从对象的字典中取/存/删;例如 a.x 的查找链是:先查 a.__dict__['x'],再查 type(a).__dict__['x'],然后沿着 type(a) 的方法解析顺序(MRO)继续。如果查到的值定义了描述符方法之一,Python 可能覆盖默认行为转而调用描述符方法——具体在优先链中的哪个位置,取决于定义了哪些描述符方法。
按方法组合分为两类:
- 数据描述符(data descriptor):定义了
__set__或__delete__。若实例字典中有同名条目,数据描述符优先。 - 非数据描述符(non-data descriptor):只定义
__get__(方法就是典型用法)。若实例字典中有同名条目,字典条目优先。
想造一个只读数据描述符,只需让 __set__ 抛出 AttributeError——仅定义一个抛异常的 __set__ 占位方法,就足以使其成为数据描述符。
实例查找的完整优先级
obj.x 在 obj 的命名空间链中查找属性;若搜索在实例 __dict__ 之外找到描述符,则按以下优先级规则调用其 __get__:
数据描述符 > 实例变量 > 非数据描述符 > 类变量 > __getattr__(若定义)
点号查找的逻辑位于 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 = sentinel('null')
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) # 数据描述符
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # 实例变量
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # 非数据描述符
if cls_var is not null:
return cls_var # 类变量
raise AttributeError(name)
官方文档用 DualOperator / DualOperatorWithSlots 等测试类(点号访问 vs 括号访问结果一致)验证了这段模拟与真实 object.__getattribute__ 的保真度,覆盖场景包括:类变量 x、实例变量 z、property(p2/p3,其中 p3 被实例字典条目覆盖)、普通方法(m5/m7)、__getattr__ 钩子、__slots__ 类以及继承自基类的描述符(U2 继承 U1 的 D1 描述符)。
从源码结构看,文档注释中提到的 _PyType_Lookup() 在当前 CPython 树中确实存在(Objects/typeobject.c),它通过 MRO 逐级查类字典(tp_dict),是 find_name_in_mro 的 C 实现原型;通用属性设置的 C 实现则位于 Objects/object.c 的 _PyObject_GenericSetAttrWithDict() 等函数中。
一个容易踩坑的细节:__getattribute__ 的代码里没有 __getattr__ 钩子——所以直接调用 __getattribute__(或 super().__getattribute__)会完全绕过 __getattr__。真正负责在 __getattribute__ 抛出 AttributeError 时调用 __getattr__ 的是点号运算符和 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__
>>> class ClassWithGetAttr:
... x = 123
... def __getattr__(self, attr):
... return attr.upper()
...
>>> cw = ClassWithGetAttr()
>>> cw.y = 456
>>> getattr_hook(cw, 'x')
123
>>> getattr_hook(cw, 'y')
456
>>> getattr_hook(cw, 'z')
'Z'
从类查找与从 super 查找
- 从类查找(
A.x):逻辑位于type.__getattribute__,步骤与object.__getattribute__类似,只是把"实例字典查找"换成"沿 MRO 搜索"。找到描述符时以desc.__get__(None, A)调用(实例参数为None)。C 实现在 Objects/typeobject.c 的_Py_type_getattro(当前版本的tp_getattro入口,见 Objects/typeobject.c#L6414-L6429)与_PyType_Lookup中。 - 从 super 查找(
super(A, obj).m):逻辑位于super()返回对象的__getattribute__方法。查找obj.__class__.__mro__中紧跟在A之后的基类B,然后返回B.__dict__['m'].__get__(obj, A);若m不是描述符则原样返回。C 实现是super_getattro(Objects/typeobject.c#L12624)。
调用逻辑总结
描述符机制嵌入在 object、type 与 super 三者的 __getattribute__ 方法中。要记住的要点:
- 描述符由
__getattribute__方法调用; - 类从
object、type或super继承这套机制; - 重写
__getattribute__会导致自动描述符调用失效,因为全部描述符逻辑都集中在那个方法里; object.__getattribute__与type.__getattribute__对__get__的调用方式不同:前者带上实例(可能带类),后者实例位置传None且总是带类;- 数据描述符永远覆盖实例字典;非数据描述符可能被实例字典覆盖。
自动名称通知
有时描述符需要知道自己被赋给了哪个类变量名。创建新类时,元类 type 扫描新类字典;若某个条目是定义了 __set_name__ 的描述符,就以两个参数调用它:owner 是描述符所在的类,name 是被赋值的类变量名。
实现细节位于 Objects/typeobject.c:type_new() 构建完类型后调用 type_new_set_names(),后者复制类字典并逐项检查 __set_name__。由于更新逻辑在 type.__new__ 中,通知只在类创建时发生——之后动态添加的描述符需要手动调用 __set_name__。
ORM 示例:描述符实现对象关系映射
官方文档用一个简化骨架展示如何用数据描述符实现 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 定义描述各表 schema 的模型:
class Movie:
table = 'Movies' # 表名
key = 'title' # 主键
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
连接数据库(文档演示用 sqlite3.connect('entertainment.db'),官方测试 setup 中用的是内存库 :memory: 并预置了 Music 与 Movies 两张表及索引):
>>> 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'
注意 __set_name__ 在这里的关键作用:owner.table 和 owner.key 必须在类创建时(此时 table/key 类变量已就绪)就能访问,因此 SQL 语句的组装放在 __set_name__ 而非 __init__ 里——这也是"描述符需要知道宿主类时"的标准解法。
纯 Python 等价实现:拆解内置描述符
描述符协议简单却蕴含丰富的可能性。有若干常见用法被打包成内置工具:property、绑定方法、静态方法、类方法、__slots__ 都基于描述符协议。
property:访问即调用的数据描述符
调用 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.")
>>> C.x.__doc__
"I'm the 'x' property."
>>> c.x = 2.71828
>>> c.x
2.71828
>>> del c.x
>>> c.x
AttributeError: 'C' object has no attribute '_C__x'
property 的 C 类型定义是 Objects/descrobject.c 中的 PyProperty_Type(见 Objects/descrobject.c#L2062),其 getter/setter/deleter 辅助方法也是描述符方法之外的普通方法。纯 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
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
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError
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__)
注意几个行为细节:
__get__中obj is None时返回self——这就是为什么C.x拿到的是 property 对象本身;fget/fset/fdel缺省为None时,对应操作抛AttributeError(这正是"只读 property"的原理,错误信息形如property 'dept' of 'Immutable' object has no setter);doc未显式给出时自动取自fget.__doc__;- 装饰器风格(
@x.setter/@x.deleter)等价于函数式写法,官方测试对两种风格验证了完全一致的行为(设置/读取/删除、hasattr语义、缺失 getter/setter/deleter 时抛AttributeError)。
官方文档给出的动机场景:当接口已经授予了属性访问权(例如电子表格类的 Cell('b10').value),后续演进又要求每次访问时做额外工作(如重新计算单元格),但不想破坏既有客户端代码——把该属性包进 property 数据描述符即可平滑升级:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
内置 property 或上面的 Property 等价类在这个场景中都适用。
函数与绑定方法:self 从哪里来
Python 的面向对象特性构建在函数式环境之上,两者通过非数据描述符无缝融合:存放在类字典中的函数在被访问时变成方法;方法与普通函数的唯一区别是对象实例会被前置到参数列表(惯例上叫 self)。
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)
def __getattribute__(self, name):
"Emulate method_getset() in Objects/classobject.c"
if name == '__doc__':
return self.__func__.__doc__
return object.__getattribute__(self, name)
def __getattr__(self, name):
"Emulate method_getattro() in Objects/classobject.c"
return getattr(self.__func__, name)
def __get__(self, obj, objtype=None):
"Emulate method_descr_get() in Objects/classobject.c"
return self
为支持方法的自动创建,函数本身带有 __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)
在 CPython 中,这个描述符协议方法的 C 实现就是 Objects/funcobject.c 里的 func_descr_get()(Objects/funcobject.c#L1267),它作为 tp_descr_get 槽(见 Objects/funcobject.c#L1310)注册在函数类型上。三种访问路径的行为:
>>> 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 从何而来——就是这里!"官方文档还验证了手工绑定的等价性:
>>> d2.f = d.f.__get__(d2, D2)
>>> d2.f() is d
True
三种方法的绑定对照
非数据描述符提供了对"函数绑定为方法"这一模式的简单变体机制。函数有 __get__,可以把 obj.f(*args) 调用转换成 f(obj, *args);而 cls.f(*args) 保持 f(*args)。官方文档的对照表:
| 变换 | 从对象调用 | 从类调用 |
|---|---|---|
| function | f(obj, *args) |
f(*args) |
| staticmethod | f(*args) |
f(*args) |
| classmethod | f(type(obj), *args) |
f(cls, *args) |
staticmethod
静态方法原样返回底层函数,c.f 与 C.f 等价于直接查找,函数从对象和类以完全相同的方式可访问。适合不引用 self 的方法——例如统计包里与特定数据集无关但概念相关的工具函数 erf(x),可以从对象或类调用。
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
纯 Python 版本的 staticmethod:
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)
@property
def __annotations__(self):
return self.f.__annotations__
functools.update_wrapper() 的作用值得强调:它添加了指向底层函数的 __wrapped__ 属性,并携带 __name__、__qualname__、__doc__、__module__ 等属性,使包装器"看起来就是"被包装的函数。官方测试验证了 StaticMethod 与 staticmethod 的等价性:
>>> sm = vars(E_sim)['f'] # 类字典里是 StaticMethod 实例
>>> type(sm).__name__
'StaticMethod'
>>> f = E_sim.f # 点号访问后是普通函数
>>> type(f).__name__
'function'
>>> sm.__name__ == f.__name__
True
>>> sm.__qualname__ == f.__qualname__
True
>>> sm(3) == f(3)
True
classmethod
与静态方法不同,类方法在调用前把类引用前置到参数列表,无论调用方是对象还是类,形式都相同:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
当方法只需要类引用而不依赖特定实例数据时就有用,典型用途是替代类构造器——例如 dict.fromkeys 从键列表创建新字典:
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}
纯 Python 版本的 classmethod:
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)
return MethodType(self.f, cls)
关键差异在于绑定目标:function.__get__ 绑定的是实例 obj,ClassMethod.__get__ 绑定的却是类 cls(实例调用时 cls 为 None,则取 type(obj))。官方测试确认了从类和从实例两种调用方式都得到相同结果,且 update_wrapper() 正确复制了 __name__、__qualname__、__doc__、__annotations__,__wrapped__ 可直接用于调用底层函数:
>>> T.cm(11, 22)
('T', 11, 22)
>>> t = T()
>>> t.cm(11, 22)
('T', 11, 22)
>>> f = vars(T)['cm'].__wrapped__
>>> f(T, 11, 22)
('T', 11, 22)
成员对象与 slots
当类定义 __slots__ 时,实例字典被替换为一个定长的槽值数组。对用户而言有五个影响:
- 立即发现拼写错误的属性赋值 bug——只允许
__slots__中列出的属性名:
class Vehicle:
__slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
- 便于创建不可变对象,由描述符管理对槽中私有属性的访问:
class Immutable:
__slots__ = ('_dept', '_name') # 替换实例字典
def __init__(self, dept, name):
self._dept = dept # 存到私有属性
self._name = name
@property # 只读描述符
def dept(self):
return self._dept
@property
def name(self): # 只读描述符
return self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
AttributeError: property 'dept' of 'Immutable' object has no setter
>>> mark.location = 'Mars'
AttributeError: 'Immutable' object has no attribute 'location'
- 节省内存。按官方文档的实测数据(64 位 Linux 构建),含两个属性的实例用
__slots__占 48 字节,不用则占 152 字节。这种"享元(flyweight)"设计只有在创建大量实例时才真正重要。 - 提升速度。按官方文档的测量(Python 3.10,Apple M1 处理器),读取实例变量快 35%。
- 阻断需要实例字典的工具,例如
functools.cached_property:
from functools import cached_property
class CP:
__slots__ = () # 消除实例字典
@cached_property # 需要实例字典
def pi(self):
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
for n in reversed(range(100_000)))
>>> CP().pi
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
由于 __slots__ 需要直接访问 C 结构并控制对象内存分配,无法做出精确的纯 Python 替代;但官方文档构建了一个基本忠实的模拟:真实的 C 槽结构由私有列表 _slotvalues 模拟,对该结构的读写由**成员描述符(member descriptor)**管理:
null = sentinel('null')
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/descrobject.h'
# 另见 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'
# 另见 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() 调用 PyTypeReady(),后者调用 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__ 负责创建带槽而非实例字典的实例(粗略模拟):
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 装载了成员对象(vars(H) 中可见 'x': <Member 'x' of 'H'>)。创建实例后,属性存于 slot_values 列表:
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
# 拼写错误或未赋值的属性会抛异常
>>> h.xz
AttributeError: 'H' object has no attribute 'xz'
官方测试还覆盖了两类边界情况:del h.x 之后 hasattr(h, 'x') 为 False;未初始化槽的实例对 hasattr(hu, 'x') 也返回 False——模拟的 Member.__get__ 通过把 null 哨兵视为"未赋值"来复现真实语义。
源码级佐证:描述符机制在 CPython 中的落点
将上述纯 Python 模拟与 CPython 当前源码(本仓库版本 3.16.0a0,见 Include/patchlevel.h#L30)对照,可以确认文档所述各机制的真实实现位置:
| 文档所述机制 | 源码位置 |
|---|---|
MRO 名称查找(find_name_in_mro 模拟的 _PyType_Lookup) |
Objects/typeobject.c#L6268-L6276 |
类的属性查找入口 _Py_type_getattro(tp_getattro) |
Objects/typeobject.c#L6414-L6429,槽注册见 Objects/typeobject.c#L7191 |
super() 的属性查找 super_getattro |
Objects/typeobject.c#L12624 |
__set_name__ 自动通知 type_new_set_names |
Objects/typeobject.c#L12211-L12253 |
函数的描述符协议方法 func_descr_get(self 绑定点) |
Objects/funcobject.c#L1267 |
property 的 C 类型 PyProperty_Type |
Objects/descrobject.c#L2062 |
通用属性设置 _PyObject_GenericSetAttrWithDict |
Objects/object.c |
值得注意的实现细节:
- 描述符分派的"双重检查"与文档模拟一致。
_Py_type_getattro(类属性查找)内部先做 MRO 查找,再检查查到的对象是否为描述符——与object_getattribute模拟中"先find_name_in_mro、再取type(cls_var).__get__"的次序相同。 __set_name__回调发生在type_new成功路径上。从源码结构看,type_new()在构造完类型后调用type_new_set_names(type)(Objects/typeobject.c#L4943),逐条目以PyObject_CallFunctionObjArgs(set_name, type, key, NULL)调用——这解释了为什么事后动态添加的描述符收不到通知。func_descr_get就是"函数 → 绑定方法"的唯一枢纽。tp_descr_get槽在 Objects/funcobject.c#L1310 指向它:obj is None时返回函数本身,否则构造绑定方法。文档中d.f.__func__/d.f.__self__的展示,对应的正是绑定方法保存这两个引用的事实。
这些位置印证了官方文档的核心论断:描述符机制"嵌入在 object、type、super 的 __getattribute__ 中",且各查找入口对 __get__ 的传参规则(实例 vs None)由具体实现决定。
总结
回到 Descriptor Guide 的知识脉络:
- 最小实现:只要定义
__get__,对象即成为描述符;必须作为类变量使用,值按需计算而非存储; - 实用模式:动态查找(
DirectorySize)、访问日志/受管属性(LoggedAgeAccess)、配合__set_name__消除硬编码名称(LoggedAccess); - 生产级应用:校验器(
Validator+OneOf/Number/String)在数据源头阻止损坏;ORMField用数据描述符桥接外部存储; - 协议语义:数据描述符压过实例字典,非数据描述符让位于实例字典;
__getattribute__是唯一集中了全部描述符逻辑的方法,重写它会使自动描述符调用失效;__getattr__是点号/getattr层面的后备钩子; - 内置工具拆解:
property是四参数据描述符;函数是返回绑定方法的非数据描述符(func_descr_get即self的诞生地);staticmethod原样透传,classmethod把绑定目标换成类;__slots__用成员描述符管理定长槽数组,换来拼写即时报错、不可变对象、48 vs 152 字节的内存差与更快的读取,代价是无法使用cached_property等依赖实例字典的特性。
掌握这套机制后,阅读 CPython 源码(Objects/typeobject.c、Objects/funcobject.c、Objects/descrobject.c)中属性查找相关函数时,可以把 C 实现逐行对回本文的纯 Python 模拟——这正是官方文档推荐的"从行为到实现"的学习路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00