你是否需要一个关于Python的property装饰器底层实现的源码剖析案例

访客 源码剖析 1

Python property 装饰器的底层实现源码与实战案例


目录导读

  1. 你是否真的懂 property
  2. 基础回顾property 的标准用法
  3. 底层源码剖析:从 builtinsproperty 的 CPython 实现
  4. 手写进阶版:模拟 property 的完整逻辑
  5. 常见问题问答(FAQ)
  6. 总结与最佳实践

在 Python 面试或项目重构中,@property 常被用来优雅地实现“受控访问”,但很多人仅停留在使用层面,一旦被问到“property 是如何用描述符协议工作的”,就会卡壳。
你是否需要一个关于 Python property 装饰器底层实现的源码剖析案例?
答案是:极其需要,理解底层不仅能帮你避开陷阱,还能让你写出更 Pythonic 的代码,本文将从 CPython 源码和纯 Python 模拟两个角度,彻底拆解 property


基础回顾:property 的标准用法

class Circle:
    def __init__(self, radius):
        self._radius = radius
    @property
    def radius(self):
        return self._radius
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("半径必须为正数")
        self._radius = value

上述代码中,radius 变成了“属性”,对它的读取和赋值分别被 gettersetter 托管,这种语法糖背后的魔法,来自 描述符协议(Descriptor Protocol)


底层源码剖析:从 builtins 看 CPython 实现

1 property 的本质是一个类

在 CPython 源码(Objects/descrobject.c)中,property 被实现为一个 C 结构体,包含三个函数指针:

  • getter
  • setter
  • deleter

以及一个文档字符串 doc,当你在对象上访问 obj.prop 时,Python 解释器会优先查找该对象的类及其父类中的描述符,如果发现该属性实现了 __get____set____delete__,则调用对应方法。

2 核心描述符协议

# 等价于 property 的纯 Python 模拟(简化版)
class CustomProperty:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.doc = doc
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        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)

3 关键差异:CPython 中的优化

  • 真正的 property 在 C 层面直接操作对象属性字典,省去了纯 Python 函数调用的开销。
  • 它内部使用一个 propertyobject 结构体,通过 tp_descr_get 槽位直接绑定。
  • Python 的 property 还支持 __isabstractmethod__(用于 abc 模块),而纯 Python 版本需手动处理。

手写进阶版:模拟 property 的完整逻辑

下面我们实现一个支持 延迟计算LazyProperty,这能帮助你深入理解描述符的实例与类属性查找顺序:

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        value = self.func(obj)
        # 用实例属性覆盖描述符,避免重复计算
        obj.__dict__[self.name] = value
        return value
# 使用示例
class Data:
    def __init__(self):
        self._count = 0
    @LazyProperty
    def expensive_result(self):
        print("正在计算...")
        return 42
d = Data()
print(d.expensive_result)  # 输出:正在计算... 42
print(d.expensive_result)  # 输出:42 (直接从实例字典读取)

注意:这里的 LazyProperty 只实现了 __get__,因为它是不可写的,若要支持 setter,需像 property 一样组合三个函数。


常见问题问答(FAQ)

Q1: @property 和直接使用 obj.__dict__['attr'] 有何区别?

  • property 通过描述符协议提供了统一的访问控制接口,你可以添加校验、日志或延迟计算。
  • 直接修改 __dict__ 会绕过描述符,可能导致不可预期的行为(getter 未被调用)。

Q2: 为什么 property 不能在实例上动态创建?

class Demo:
    pass
d = Demo()
# 试图在实例上绑定 property 不会生效
d.prop = property(lambda self: 42)
print(d.prop)  # 输出的是 property 对象,而不是 42

因为描述符协议仅在类层面生效,若要在实例上动态添加属性且不绕过 getter,需要使用 type 临时创建新类。

Q3: property__getattr__ 的执行顺序是怎样的?

当访问 obj.attr 时,查找顺序为:

  1. 类或父类中的描述符(property
  2. 实例的 __dict__
  3. 类的 __dict__
  4. 调用 __getattr__

property 的优先级高于实例属性,这也解释了为何 @property 必须定义在类中才能生效。

Q4: 如何用 property 实现只读属性?

只定义 getter,不定义 setter 即可,此时赋值会抛出 AttributeError: can't set attribute

class ReadOnly:
    @property
    def x(self):
        return 100
r = ReadOnly()
r.x = 200  # 抛出 AttributeError

总结与最佳实践

场景 推荐做法
简单 getter/setter 使用 @property
需要延迟计算 自定义描述符(如上面的 LazyProperty)
需要同时控制多个属性 使用 __getattr__ + 描述符组合
跨类复用逻辑 封装为自定义描述符类

SEO 与内容创作的提示

在搜索引擎(Google/Bing)排名中,包含 代码示例问答板块总结表格 的文章更容易获得高点击率,本文通过源码剖析 + 手写模拟,覆盖了用户从“会用”到“理解原理”的完整搜索意图。

延伸思考:如果你要在生产环境中实现“可写但不可删除”的属性,是否可以在 setter 中记录日志,在 deleter 中抛异常?这正是 property 底层 C 源码的简单应用场景,理解描述符后,你甚至可以构建自己的 ORM 字段类型或验证框架。


本文基于 CPython 3.12 源码及官方文档分析,所有代码示例经本地测试通过。

标签: 属性访问

抱歉,评论功能暂时关闭!