文章目录

Python的descriptor协议与@property底层实现

发布于 2026-05-31 18:22:21 · 浏览 21 次 · 评论 0 条

Python的descriptor协议与@property底层实现

Python中的@property装饰器能让我们将方法调用伪装成属性访问,简化接口。但你是否好奇过,这优雅的语法糖背后究竟是如何运作的?答案就隐藏在Python一个强大而底层的机制——descriptor protocol(描述符协议) 中。理解它,不仅能解开@property之谜,更能让你洞悉类方法、静态方法乃至对象属性绑定的核心原理。


第一步:理解描述符协议的基本构成

一个对象只要实现了__get____set____delete__这三个特殊方法中的至少一个,它就被称为一个描述符。这些方法在属性被访问时会被解释器自动调用。

  1. 定义一个描述符类。描述符通常是独立于使用它的类的类。它实现了管理属性访问逻辑的方法。
class MyDescriptor:
    """一个最简单的描述符,仅演示__get__方法"""
    def __init__(self, initial_value=None):
        self.value = initial_value

    def __get__(self, instance, owner_class):
        # instance: 拥有该属性的类实例 (例如obj)
        # owner_class: 拥有该属性的类 (例如MyClass)
        # 如果通过类本身访问 (MyClass.descriptor), instance 为 None
        print(f"__get__ called. instance={instance}, owner={owner_class}")
        return self.value
  1. 在一个目标类中使用描述符。将描述符实例赋值为类属性,它就会接管对该属性名的访问。
class MyClass:
    # 将MyDescriptor的实例作为类属性
    descriptor = MyDescriptor(10)

obj = MyClass()
# 访问 obj.descriptor 时,实际触发的是 MyDescriptor.__get__(obj, MyClass)
print(obj.descriptor)  # 输出:__get__ called. ... 然后输出 10

关键点:描述符是类级别的属性。当你通过实例obj.descriptor访问时,Python会在MyClass的字典中找到descriptor这个描述符对象,然后调用它的__get__方法。


第二步:深入getset方法,实现属性控制

描述符的真正威力在于能拦截读(__get__)和写(__set__)操作,实现数据验证、缓存等高级逻辑。

  1. 实现一个带验证的描述符。以下描述符确保存储的值必须是整数。
class ValidatedInteger:
    def __init__(self, min_val=None, max_val=None):
        self.min_val = min_val
        self.max_val = max_val
        self.storage_name = None  # 用于存储实际数据的键名,稍后设置

    def __set_name__(self, owner_class, name):
        # Python 3.6+ 支持此方法。当描述符被实例化并赋值给类属性时自动调用。
        # owner_class: 描述符所属的类 (例如 Person)
        # name: 该类属性名 (例如 'age')
        # 我们用它来确定在实例字典中存储数据的键名
        self.storage_name = name

    def __set__(self, instance, value):
        # instance: 要设置属性的实例 (例如 person_obj)
        # value: 赋予的新值
        if not isinstance(value, int):
            raise TypeError(f"Expected an int for '{self.storage_name}'")
        if self.min_val is not None and value < self.min_val:
            raise ValueError(f"'{self.storage_name}' must be >= {self.min_val}")
        if self.max_val is not None and value > self.max_val:
            raise ValueError(f"'{self.storage_name}' must be <= {self.max_val}")
        # 将值存储在实例的__dict__中,键名用storage_name
        instance.__dict__[self.storage_name] = value

    def __get__(self, instance, owner_class):
        # 如果通过类访问,返回描述符本身
        if instance is None:
            return self
        # 从实例字典中取值,而不是描述符自身的值
        return instance.__dict__.get(self.storage_name, None)
  1. 在业务类中使用验证描述符。现在,Person类的age属性将自动获得验证功能。
class Person:
    age = ValidatedInteger(min_val=0, max_val=150) # 使用描述符作为类属性

p = Person()
p.age = 30          # 触发 ValidatedInteger.__set__(p, 30),验证通过
print(p.age)        # 触发 ValidatedInteger.__get__(p, Person),返回 30
p.age = -5          # 触发 __set__,抛出 ValueError

注意:在__get__方法中,我们返回的是存储在instance.__dict__中的值,而非描述符自身的value。这样可以保证每个实例的数据是独立的。


第三步:剖析@property装饰器的底层实现

@property的本质,就是创建了一个名为property的内置描述符类的实例。property类完整实现了__get____set____delete__方法,将它们转发到你定义的函数上。

  1. 查看@property的源代码逻辑。下面是一个简化版的等价实现,帮助理解其原理。
class Property:
    """模拟内置property类的简化版"""
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget      # 对应 @property 装饰的方法
        self.fset = fset      # 对应 @<name>.setter 装饰的方法
        self.fdel = fdel      # 对应 @<name>.deleter 装饰的方法
        self.__doc__ = doc or (fget and fget.__doc__)

    def __get__(self, instance, owner_class):
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        # 核心:调用原始的getter函数,并传入实例作为参数
        return self.fget(instance)

    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("can‘t set attribute")
        # 核心:调用原始的setter函数,并传入实例和值
        self.fset(instance, value)

    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("can‘t delete attribute")
        self.fdel(instance)

    def getter(self, fget):
        # 返回一个新的Property对象,只更新了fget
        return Property(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return Property(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return Property(self.fget, self.fset, fdel, self.__doc__)
  1. 还原@property的语法。当你写下以下代码时:
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """The radius of the circle."""
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius must be non-negative")
        self._radius = value

Python解释器实际执行的操作类似于:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    def get_radius(self):
        return self._radius

    def set_radius(self, value):
        if value < 0:
            raise ValueError("Radius must be non-negative")
        self._radius = value

    # 关键步骤:手动创建property描述符实例并赋给类属性`radius`
    radius = property(fget=get_radius, fset=set_radius, doc="The radius of the circle.")

结论@property语法糖是创建了一个特定的描述符实例(property对象),这个实例将__get____set__方法分别绑定到你编写的gettersetter函数上。


第四步:理解数据描述符与非数据描述符的优先级

描述符分为两类,它们与实例字典(__dict__)的优先级关系是理解属性查找顺序的关键。

  1. 区分描述符类型

    • 数据描述符:同时定义了__get____set__(或__delete__)方法的描述符。例如我们上面创建的ValidatedInteger和内置的property
    • 非数据描述符:只定义了__get__方法的描述符。例如内置的staticmethodclassmethod以及我们第一步中的MyDescriptor
  2. 掌握属性查找顺序。当通过实例obj.attr访问属性时,Python的查找顺序如下:

    1. 首先在类及其父类的MRO链中查找attr
      • 如果找到的是一个数据描述符,则调用其__get__方法,并忽略实例字典
      • 如果找到的是一个非数据描述符或一个普通类属性,记录下来,进入下一步。
    2. 然后在实例的字典__dict__中查找attr
      • 如果找到,直接返回该值。
      • 如果未找到,且第1步记录的是一个非数据描述符或普通类属性,则返回记录的值(调用__get__或直接返回)。

这个顺序解释了为什么@property能生效。因为property是数据描述符,它的优先级高于实例字典。即使实例字典里有同名属性(如obj.__dict__['radius'] = 5),访问obj.radius仍会触发property__get__

  1. 验证优先级。看一个简单的例子。
class A:
    def __init__(self):
        self.x = 10 # 实例字典里设置 x

    @property
    def x(self): # 数据描述符
        return "I am the property"

a = A()
print(a.__dict__) # 输出:{'x': 10}
print(a.x) # 输出:I am the property (数据描述符优先)

实用指南总结:何时以及如何使用描述符

  1. 何时自定义描述符

    • 需要在多个不同的类中复用相同的属性访问逻辑(如类型检查、数值范围验证、懒加载计算)。
    • 需要更精细地控制属性的获取、设置和删除行为,超出了@property单个装饰器的范围。
    • 正在构建库或框架,需要提供灵活的属性注入机制。
  2. 使用@property的场景

    • 为简单的属性添加只读、计算属性或基本的验证逻辑。
    • 当逻辑只与当前类相关,且无需在其他地方复用时。
  3. 核心实现步骤

    • 定义一个类,并在其中实现__get__(self, instance, owner)和/或__set__(self, instance, value)方法。
    • 使用__set_name__(self, owner, name)方法(可选,但推荐)自动获取属性名,用于在实例字典中存储数据。
    • 在业务类中,将描述符实例赋值给一个类属性。
    • 通过该类的实例正常访问该属性名,即可触发描述符的方法。
# 一个完整的、支持类型转换的描述符示例
class Coerce:
    def __init__(self, target_type):
        self.target_type = target_type

    def __set_name__(self, owner_class, name):
        self.name = name
        self.storage_name = f'_{name}'

    def __set__(self, instance, value):
        # 强制类型转换后存储
        instance.__dict__[self.storage_name] = self.target_type(value)

    def __get__(self, instance, owner_class):
        if instance is None:
            return self
        # 从实例字典中获取已转换的值
        return instance.__dict__.get(self.storage_name, None)

class UserProfile:
    name = Coerce(str)    # 描述符实例
    age = Coerce(int)     # 另一个描述符实例

user = UserProfile()
user.name = 123       # 自动转为字符串 '123'
user.age = '30'       # 自动转为整数 30
print(user.name, user.age) # 输出:123 30

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文