深入解析Python Member Descriptor 描述符
作者:无风听海
1. 什么是 Descriptor(描述符)
在 Python 中,描述符是实现了描述符协议的对象。描述符协议由三个方法组成:
__get__(self, obj, objtype=None)→ 获取属性值__set__(self, obj, value)→ 设置属性值__delete__(self, obj)→ 删除属性
只要一个对象定义了以上任意一个方法,它就是一个描述符。描述符是 Python 属性访问机制的底层基础,property、classmethod、staticmethod、slot 等都依赖描述符实现。
2. Member Descriptor 的本质
Member Descriptor(member_descriptor)是 CPython 内部的一种描述符类型,当类使用 __slots__ 时,Python 为每个 slot 自动生成一个 member_descriptor 对象。它直接操作实例的内存布局,无需 __dict__,因此访问速度极快。
class Point:
__slots__ = ('x', 'y')
# 查看类属性
print(type(Point.x))
# <class 'member_descriptor'>
print(type(Point.y))
# <class 'member_descriptor'>
member_descriptor 在 C 层面对应 PyMemberDescrObject,定义在 Objects/descrobject.c 中。它通过固定偏移量(offset)直接访问实例内存中的字段,绕过了字典查找。
3. Member Descriptor vs 其他描述符类型
Python 内置了多种描述符类型,它们的区别如下:
| 类型 | 来源 | 实现 |
|---|---|---|
| member_descriptor | __slots__ | C 层面,按偏移量存取 |
| property | @property 装饰器 | Python 层面,调用 getter/setter |
| getset_descriptor | C 扩展类型的 tp_getset | C 层面,调用 getter/setter 函数指针 |
| wrapper_descriptor | C 类型的方法(如 list.append) | C 层面 |
class WithSlots:
__slots__ = ('value',)
class WithProperty:
@property
def value(self):
return self._value
import types
print(type(WithSlots.value)) # <class 'member_descriptor'>
print(type(WithProperty.value)) # <class 'property'>
# getset_descriptor 的例子(内置类型)
print(type(type.__dict__['__dict__'])) # <class 'getset_descriptor'>
4. 描述符协议的调用机制
当我们访问 obj.attr 时,Python 的属性查找遵循以下优先级:
- Data descriptor(同时定义
__get__和__set__)优先于实例__dict__ - 实例
__dict__优先于 Non-data descriptor(只定义__get__) - 如果以上都没找到,调用
__getattr__
member_descriptor 是一个 data descriptor,因为它同时实现了 __get__、__set__ 和 __delete__:
class Demo:
__slots__ = ('name',)
d = Demo()
# __set__
Demo.name.__set__(d, "hello")
print(d.name) # hello
# __get__
print(Demo.name.__get__(d, Demo)) # hello
# __delete__
Demo.name.__delete__(d)
# print(d.name) # AttributeError: name
5. 内存布局与性能优势
member_descriptor 直接通过内存偏移量访问数据,这带来了显著的性能优势:
import sys
class WithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
d = WithDict(1, 2)
s = WithSlots(1, 2)
print(sys.getsizeof(d) + sys.getsizeof(d.__dict__)) # ~152 bytes (取决于版本)
print(sys.getsizeof(s)) # ~56 bytes
# 性能基准测试
import timeit
setup_dict = "from __main__ import WithDict; obj = WithDict(1, 2)"
setup_slots = "from __main__ import WithSlots; obj = WithSlots(1, 2)"
t_dict = timeit.timeit("obj.x", setup=setup_dict, number=10_000_000)
t_slots = timeit.timeit("obj.x", setup=setup_slots, number=10_000_000)
print(f"dict access: {t_dict:.3f}s")
print(f"slots access: {t_slots:.3f}s")
# slots 通常快 10-30%
CPython 在编译 class 时会为每个 slot 分配一个 Py_ssize_t offset,member_descriptor 使用这个偏移量直接计算指针位置:
// CPython 内部伪代码
static PyObject *
member_get(PyMemberDescrObject *descr, PyObject *obj) {
char *addr = (char *)obj + descr->d_member->offset;
return *(PyObject **)addr;
}
6. 自定义实现一个类似 Member Descriptor 的描述符
理解了底层机制后,我们可以用纯 Python 模拟 member_descriptor 的行为:
class MemberDescriptor:
"""模拟 CPython 的 member_descriptor"""
# 用于区分 "未设置" 和 "设置为 None"
_MISSING = object()
def __init__(self, name):
self.name = name
self.internal_name = f"_slot_{name}"
def __set_name__(self, owner, name):
"""Python 3.6+ 自动调用,获取属性名"""
self.name = name
self.internal_name = f"_slot_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
# 通过类访问时返回描述符本身
return self
value = obj.__dict__.get(self.internal_name, self._MISSING)
if value is self._MISSING:
raise AttributeError(
f"'{type(obj).__name__}' object has no attribute '{self.name}'"
)
return value
def __set__(self, obj, value):
obj.__dict__[self.internal_name] = value
def __delete__(self, obj):
if self.internal_name not in obj.__dict__:
raise AttributeError(
f"'{type(obj).__name__}' object has no attribute '{self.name}'"
)
del obj.__dict__[self.internal_name]
def __repr__(self):
return f"<member '{self.name}'>"
class Vector:
x = MemberDescriptor('x')
y = MemberDescriptor('y')
def __init__(self, x, y):
self.x = x
self.y = y
v = Vector(3, 4)
print(v.x) # 3
print(Vector.x) # <member 'x'>
del v.x
try:
print(v.x)
except AttributeError as e:
print(e) # 'Vector' object has no attribute 'x'
7. Member Descriptor 与继承
__slots__ 和 member_descriptor 在继承场景下有特殊行为:
class Base:
__slots__ = ('x',)
class Child(Base):
__slots__ = ('y',)
c = Child()
c.x = 1
c.y = 2
# 每个类只拥有自己声明的 slot 对应的 member_descriptor
print('x' in Base.__dict__) # True
print('x' in Child.__dict__) # False — 继承自 Base
print('y' in Child.__dict__) # True
# 重复声明 slot 会创建独立的 member_descriptor(浪费内存!)
class BadChild(Base):
__slots__ = ('x', 'z') # x 重复了
print(Base.__dict__['x']) # <member 'x' of 'Base' objects>
print(BadChild.__dict__['x']) # <member 'x' of 'BadChild' objects>
# 两个不同的 descriptor,Base.x 被 BadChild.x 遮蔽
8. Member Descriptor 的元信息
每个 member_descriptor 携带了描述性元信息:
class Config:
__slots__ = ('host', 'port')
desc = Config.__dict__['host']
print(desc.__objclass__) # <class 'Config'> — 所属类
print(desc.__name__) # 'host' — 属性名
print(desc.__doc__) # None(可通过 __slots__ = {'host': 'The hostname'} 设置)
# 使用 dict 形式的 __slots__ 添加文档
class ConfigDoc:
__slots__ = {
'host': 'The server hostname',
'port': 'The server port number',
}
print(ConfigDoc.host.__doc__) # 'The server hostname'
print(ConfigDoc.port.__doc__) # 'The server port number'
9. 与inspect模块的交互
import inspect
class Entity:
__slots__ = ('id', 'name')
# 判断是否为 data descriptor
def is_data_descriptor(obj):
return hasattr(obj, '__get__') and (hasattr(obj, '__set__') or hasattr(obj, '__delete__'))
print(is_data_descriptor(Entity.id)) # True
# inspect.getmembers_static 可以避免触发描述符的 __get__
for name, value in inspect.getmembers_static(Entity):
if isinstance(value, type(Entity.id)): # member_descriptor
print(f" slot: {name}")
# 输出:
# slot: id
# slot: name
10. 实际应用:结合__slots__与描述符的高性能数据类
from typing import Any
class TypedSlot:
"""带类型检查的 slot 描述符"""
def __init__(self, expected_type: type, default: Any = None):
self.expected_type = expected_type
self.default = default
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, f"_{self.name}", self.default)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"'{self.name}' expects {self.expected_type.__name__}, "
f"got {type(value).__name__}"
)
object.__setattr__(obj, f"_{self.name}", value)
def __delete__(self, obj):
try:
object.__delattr__(obj, f"_{self.name}")
except AttributeError:
raise AttributeError(f"'{self.name}' is not set")
class Connection:
__slots__ = ('_host', '_port', '_timeout')
host = TypedSlot(str, default="localhost")
port = TypedSlot(int, default=8080)
timeout = TypedSlot((int, float), default=30.0)
def __init__(self, host: str, port: int, timeout: float = 30.0):
self.host = host
self.port = port
self.timeout = timeout
conn = Connection("192.168.1.1", 443, 60.0)
print(conn.host) # 192.168.1.1
print(conn.port) # 443
print(conn.timeout) # 60.0
try:
conn.port = "not_a_number"
except TypeError as e:
print(e) # 'port' expects int, got str
11. CPython 源码层面的实现
在 CPython 源码中(Objects/descrobject.c),member_descriptor 的核心结构如下:
// Include/cpython/descrobject.h
typedef struct {
PyDescrObject d_common;
struct PyMemberDef *d_member; // 包含 name, type, offset
} PyMemberDescrObject;
// Include/structmember.h
typedef struct PyMemberDef {
const char *name;
int type; // T_OBJECT, T_INT, T_STRING 等
Py_ssize_t offset; // 在实例结构体中的偏移量
int flags; // READONLY 等标志
const char *doc;
} PyMemberDef;
关键执行路径:
// 简化的 __get__ 实现
static PyObject *
member_get(PyMemberDescrObject *descr, PyObject *obj, PyObject *type)
{
if (obj == NULL || obj == Py_None) {
Py_INCREF(descr);
return (PyObject *)descr;
}
return PyMember_GetOne((char *)obj, descr->d_member);
}
// PyMember_GetOne 根据 offset 和 type 读取值
PyObject *
PyMember_GetOne(const char *obj_char, PyMemberDef *l)
{
PyObject *v;
switch (l->type) {
case T_OBJECT:
v = *(PyObject **)(obj_char + l->offset);
if (v == NULL)
// 尚未赋值 → AttributeError
...
break;
case T_INT:
v = PyLong_FromLong(*(int *)(obj_char + l->offset));
break;
// ... 其他类型
}
return v;
}
12. 总结
| 特性 | 说明 |
|---|---|
| 本质 | C 层描述符,通过偏移量直接访问实例内存 |
| 触发条件 | 类定义 __slots__ |
| 描述符类型 | Data descriptor(实现 __get__ + __set__ + __delete__) |
| 优先级 | 高于实例 __dict__(但 slots 类通常无 __dict__) |
| 性能 | 比 __dict__ 快 10-30%,内存占用显著降低 |
| 元信息 | __name__、__objclass__、__doc__ |
| 文档化 | 使用 __slots__ = {'name': 'docstring'} 字典形式 |
member_descriptor 是 Python 对象模型中最底层、最高效的属性访问机制之一。理解它不仅有助于写出更高效的代码,也是深入理解 Python 描述符协议、属性查找链和 CPython 内部实现的重要一环。
到此这篇关于深入解析Python Member Descriptor 的文章就介绍到这了,更多相关Python Member Descriptor 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- python的描述器descriptor详解
- 一文带你搞懂Python中的描述符(Descriptor)
- Python descriptor(描述符)的实现
- Python描述符descriptor使用原理解析
- 轻松理解Python 中的 descriptor
- Python 描述符(Descriptor)入门
- 详解Python中的Descriptor描述符类
- Python中的descriptor描述器简明使用指南
- Python黑魔法Descriptor描述符的实例解析
- Python中的Descriptor描述符学习教程
- Python 的描述符 descriptor详解
- 解密Python中的描述符(descriptor)
