深入解析Python Member Descriptor 描述符

 更新时间:2026年05月14日 10:20:19   作者:无风听海  
本文主要介绍了深入解析Python Member Descriptor, 文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1. 什么是 Descriptor(描述符)

在 Python 中,描述符是实现了描述符协议的对象。描述符协议由三个方法组成:

  • __get__(self, obj, objtype=None) → 获取属性值
  • __set__(self, obj, value) → 设置属性值
  • __delete__(self, obj) → 删除属性

只要一个对象定义了以上任意一个方法,它就是一个描述符。描述符是 Python 属性访问机制的底层基础,propertyclassmethodstaticmethodslot 等都依赖描述符实现。

2. Member Descriptor 的本质

Member Descriptormember_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_descriptorC 扩展类型的 tp_getsetC 层面,调用 getter/setter 函数指针
wrapper_descriptorC 类型的方法(如 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 的属性查找遵循以下优先级:

  1. Data descriptor(同时定义 __get____set__)优先于实例 __dict__
  2. 实例 __dict__ 优先于 Non-data descriptor(只定义 __get__
  3. 如果以上都没找到,调用 __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 offsetmember_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的接口测试框架实例

    基于Python的接口测试框架实例

    下面小编就为大家带来一篇基于Python的接口测试框架实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-11-11
  • Python实现视频自动打码的示例代码

    Python实现视频自动打码的示例代码

    我们在观看视频的时候,有时候会出现一些奇怪的马赛克,影响我们的观影体验,那么这些马赛克是如何精确的加上去的呢?本文就来为大家详细讲讲
    2022-04-04
  • 如何使用Python在Excel中添加超链接

    如何使用Python在Excel中添加超链接

    本文将介绍如何使用 Python 和 Spire.XLS 库在 Excel 工作表中添加各种类型的超链接,包括网址链接、电子邮件链接、工作表内部链接以及图片超链接等,感兴趣的朋友跟随小编一起看看吧
    2026-05-05
  • python3.6使用urllib完成下载的实例

    python3.6使用urllib完成下载的实例

    今天小编就为大家分享一篇python3.6使用urllib完成下载的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-12-12
  • Pytorch 扩展Tensor维度、压缩Tensor维度的方法

    Pytorch 扩展Tensor维度、压缩Tensor维度的方法

    这篇文章主要介绍了Pytorch 扩展Tensor维度、压缩Tensor维度的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • python简单批量梯度下降代码

    python简单批量梯度下降代码

    大家好,本篇文章主要讲的是python简单批量梯度下降代码,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2022-01-01
  • Python图像运算之腐蚀与膨胀详解

    Python图像运算之腐蚀与膨胀详解

    这篇文章将详细讲解开始图像形态学知识,主要介绍图像腐蚀处理和膨胀处理。文中的示例代码简洁易懂,感兴趣的小伙伴快跟随小编一起学习一下吧
    2022-05-05
  • 使用Python和wxPython开发的Windows进程管理工具

    使用Python和wxPython开发的Windows进程管理工具

    在日常使用 Windows 系统的过程中,我们经常会遇到需要批量管理进程的场景,Windows 自带的任务管理器虽然功能强大,但在批量管理进程方面存在明显不足,因此,本文给大家介绍了如何使用Python和wxPython开发的Windows进程管理工具,需要的朋友可以参考下
    2026-01-01
  • Python画图高斯分布的示例

    Python画图高斯分布的示例

    今天小编就为大家分享一篇Python画图高斯分布的示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-07-07
  • Python如何根据页码处理PDF文件的内容

    Python如何根据页码处理PDF文件的内容

    在Python中,fitz库可以用于多种任务,如打开PDF文件、遍历页面、添加注释、提取文本、旋转页面等,此外,它还可以用于在PDF页面上添加高亮注释、提取图像等操作,这篇文章主要介绍了Python根据页码处理PDF文件的内容,需要的朋友可以参考下
    2024-06-06

最新评论