深入理解Python property底层:描述符协议的工作原理

林见深
林见深
发布时间:2026-10-22| 5人浏览过
原创

很多 Python 开发者把 @property 当作将方法伪装成属性的便利语法糖,但在语言底层,它完全依赖一套严密的通用规范——描述符协议(Descriptor Protocol)。理解描述符的运作方式,不仅能解答“为什么属性赋值能直接触发校验函数”,更能搞清 Python 对象属性访问、方法绑定及元编程的底层分发逻辑。

一、描述符协议的定义与核心方法

在 Python 中,任何实现了 __get__()、__set__() 或 __delete__() 中至少一个特殊方法的对象,都被称为描述符(Descriptor)。这三个方法构成了描述符协议的全部接口规范:

  • __get__(self, instance, owner=None):在通过实例或类读取属性时调用。instance 是访问属性的具体实例(若是通过类本身访问,则为 None),owner 为属性所在的宿主类。
  • __set__(self, instance, value):在通过实例给属性赋值时调用。value 为待赋予的目标值。
  • __delete__(self, instance):在通过实例执行 del instance.attr 时调用。

描述符本身通常作为类属性存在,而不是实例属性。当外界尝试访问 instance.attr 时,Python 底层的 object.__getattribute__ 会拦截这一请求,并根据描述符协议决定是直接返回字典中的值,还是转而调用描述符的 __get__() 方法。

深入理解Python property底层:描述符协议的工作原理
图 1:Python 内部 object.__getattribute__ 的属性分发流程图

二、数据描述符与非数据描述符的优先级之争

理解描述符协议的关键分水岭在于:数据描述符(Data Descriptor)与非数据描述符(Non-data Descriptor)的差异。

  • 数据描述符:只要定义了 __set__() 或 __delete__()(即使 __set__ 内部直接抛出异常),该对象就是数据描述符。property 属于典型的数据描述符。
  • 非数据描述符:只定义了 __get__(),没有定义修改或删除方法。普通的 Python 函数、方法、@classmethod 以及 @staticmethod 都是非数据描述符。
深入理解Python property底层:描述符协议的工作原理
图 2:数据描述符与非数据描述符核心差异及覆盖策略对比

这一机制解释了两个核心现象:

  1. 为什么只读 property 无法被赋值覆盖:当我们仅用 @property 装饰一个 getter 方法时,Python 返回的 property 实例仍然定义了 __set__(并在其中抛出 AttributeError: can't set attribute)。因此它是一个数据描述符。执行 obj.attr = 1 时,解释器会优先触发该描述符的 __set__ 并抛错,绝不会退回到向 obj.__dict__ 写入变量。
  2. 为什么实例方法可以被动态覆盖:普通方法只是非数据描述符。若你显式执行 obj.my_method = lambda: "new",该函数对象会直接存入 obj.__dict__['my_method']。下一次调用时,实例字典命中,原类方法就被遮蔽了。

三、手写简化版 Property 剖析运行机制

为了直观掌握 property 在底层如何调度 getter 与 setter,我们可以用纯 Python 编写一个功能等价的 SimpleProperty 类:

class SimpleProperty:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc or (fget.__doc__ if fget else None)

    def __get__(self, instance, owner=None):
        # 通过类本身访问 (如 Person.age),返回描述符对象自身
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("未定义 getter 方法,无法读取属性")
        # 将实例作为第一个参数传入原始 getter 函数
        return self.fget(instance)

    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("未定义 setter 方法,该属性为只读")
        # 调用被装饰的 setter 函数
        self.fset(instance, value)

    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("未定义 deleter 方法,无法删除该属性")
        self.fdel(instance)

    def setter(self, fset):
        # 返回一个新的描述符实例,将已有的 fget 与新的 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__)

测试与使用方式如下:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @SimpleProperty
    def balance(self):
        """账户余额属性"""
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("余额不能为负数")
        self._balance = value

# 测试运行
acc = Account(100)
print(acc.balance)      # 输出: 100 (调用 __get__)
acc.balance = 250       # 成功赋值 (调用 __set__)
print(acc.balance)      # 输出: 250
# acc.balance = -10     # 触发 ValueError: 余额不能为负数
深入理解Python property底层:描述符协议的工作原理
图 3:定义期类装配与运行期描述符方法分发映射关系

四、编写描述符时的三大关键细节与避坑指南

在使用描述符协议或编写自定义属性扩展时,初学者极易踩入几个隐蔽的底层陷阱:

1. 为什么描述符必须挂载在“类”上,而不能挂在“实例”上?

如果在 __init__ 中执行 self.attr = MyDescriptor(),当你调用 self.attr 时,Python 不会触发 __get__,而只是返回该描述符实例本身。

这是因为 Python 的属性查找机制 object.__getattribute__ 规定:协议钩子的检查是从 type(obj) 及其 MRO 开始寻找的。实例字典 __dict__ 里的对象只会被视作普通值,永远不会触发描述符协议。

2. 属性同名无限递归死循环

在编写 setter 或自定义描述符时,若在内部直接写 self.balance = value,会再次触发当前属性的 setter,导致 RecursionError: maximum recursion depth exceeded。标准做法是使用下划线私有变量(如 self._balance)作为底层实际存储载体。

3. 描述符自身存储数据陷阱(实例共享灾难)

由于描述符是定义在类上的单例对象,如果开发者在描述符类的 __set__ 中写下 self.val = value,那么所有宿主类实例都会共享这一份 self.val!当修改实例 A 的属性时,实例 B 的值也会被暗中篡改。正确做法是将值存放在 instance.__dict__[self.name] 中,确保每个实例的数据完全隔离。

深入理解Python property底层:描述符协议的工作原理
图 4:描述符协议四大核心原则与避坑思维总结

五、总结

Python 的 property 并非魔法,而是描述符协议的最典型应用。归纳起来,整个机制的精髓可概括为三点:

  1. 协议分级:数据描述符(包含 __set__)优先于实例字典;非数据描述符(仅 __get__)让位于实例字典。
  2. 宿主归属:描述符必须且只能定义在类上,由 object.__getattribute__ 在解析属性时主动触发。
  3. 逻辑解耦:通过把属性的读、写、删操作封装为独立的可调用对象,实现了面向对象中极高水准的封装与验证能力。

熟练掌握描述符协议,不仅能让你看懂 property、方法绑定(Method Binding)与各类装饰器的底层运作,也为你后续理解 Django ORM、SQLAlchemy 以及 Pydantic 的属性校验体系打下最坚实的基石。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。