python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python @property @cached_property

Python中@property与@cached_property的实现

作者:青衫客36

本文主要介绍了Python中@property与@cached_property的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

在 Python 中,属性不仅仅是简单的数据字段,它们还可以是动态计算的值,并且可以封装读写逻辑、懒加载、缓存控制,甚至权限检查。本文将探讨 Python 中与属性控制相关的四个关键装饰器:

一、@property—— 将方法变成属性

基本用法

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

    @property
    def radius(self):
        return self._radius

    @property
    def area(self):
        return 3.1416 * self._radius ** 2

上述代码中,radiusarea 都是方法,但通过 @property 装饰后,可以像普通属性一样使用:

c = Circle(5)
print(c.radius)  # 5
print(c.area)    # 78.54

优势

方法调用形式属性访问形式好处
obj.get_value()obj.value语义清晰、接口简洁

二、@x.setter和@x.deleter—— 为属性添加写入与删除能力

🧩 写入控制:@x.setter

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value

@name.setter 允许我们对 name 属性进行赋值时添加校验逻辑:

p = Person("Alice")
p.name = "Bob"  # 正常
p.name = ""     # 抛出异常

🧹 删除控制:@x.deleter

class Secret:
    def __init__(self):
        self._token = "abc123"

    @property
    def token(self):
        return self._token

    @token.deleter
    def token(self):
        print("Token deleted")
        del self._token
s = Secret()
del s.token  # 调用自定义的删除逻辑

三、@cached_property—— 懒加载 + 缓存

在处理高成本计算时,我们通常希望第一次调用时计算,之后读取缓存结果。这正是 @cached_property 的应用场景。

示例:懒加载计算属性

from functools import cached_property

class Expensive:
    @cached_property
    def result(self):
        print("Calculating...")
        return sum(i * i for i in range(10**6))

obj = Expensive()
print(obj.result)  # 第一次:执行计算
print(obj.result)  # 第二次:返回缓存结果

四、cached_property的底层原理

源码如下:

class cached_property:
    def __init__(self, func):
        self.func = func
        self.attrname = None
        self.__doc__ = func.__doc__
        self.lock = RLock()

    def __set_name__(self, owner, name):
        if self.attrname is None:
            self.attrname = name
        elif name != self.attrname:
            raise TypeError(
                "Cannot assign the same cached_property to two different names "
                f"({self.attrname!r} and {name!r})."
            )

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.attrname is None:
            raise TypeError(
                "Cannot use cached_property instance without calling __set_name__ on it.")
        try:
            cache = instance.__dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        val = cache.get(self.attrname, _NOT_FOUND)
        if val is _NOT_FOUND:
            with self.lock:
                # check if another thread filled cache while we awaited lock
                val = cache.get(self.attrname, _NOT_FOUND)
                if val is _NOT_FOUND:
                    val = self.func(instance)
                    try:
                        cache[self.attrname] = val
                    except TypeError:
                        msg = (
                            f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                            f"does not support item assignment for caching {self.attrname!r} property."
                        )
                        raise TypeError(msg) from None
        return val

cached_property实现了第一次调用时执行函数,并将结果存入 __dict__,之后每次访问直接从缓存中获取。

然而,笔者在debug中发现self.attrname会变成函数名,这是为什么呢?

原来从 Python 3.6 开始,描述符类可以实现 __set_name__(self, owner, name) 方法,这个方法在类定义时被自动调用,用于告诉描述符它被赋值到哪个类属性上。因为 cached_property 是一个描述符,并实现了 set_name 方法。

__set_name__(self, owner, name) 中的参数:

当 @cached_property 被装饰函数时(如 @cached_property def result(self): …),
Python 会在类创建期间(class 定义执行时)自动调用这个 __set_name__,把方法名(如 “result”)作为 name 参数传进来,从而完成 self.attrname = “result” 的绑定。

为什么这么设计?

这为 cached_property 提供了两个关键能力:

  1. 动态识别属性名,避免手动硬编码;
  2. 支持多个 cached_property 实例在一个类中使用,每个都知道自己的名字。

能力 1:动态识别属性名 ——避免手动写死名字

当我们用 @cached_property 装饰一个方法时,Python 自动把这个方法的名字(属性名)告诉它,让它知道“我是绑定在 xxx 上的”。

就像别人告诉你:“你现在的代号叫 result,记住它。”

这样一来,cached_property 就知道以后把缓存结果存在 obj.__dict__["result"] 里,而不是自己去写死 'result' 这个字符串。

如果没有这个能力,我们需要手动这样写:

class cached_property:
    def __init__(self, name, func):
        self.func = func
        self.attrname = name  # 必须手动传入
    ...

然后使用时也得这么写:

class MyClass:
    result = cached_property("result", lambda self: 42)

不仅麻烦,而且容易出错(比如名字不一致),所以 Python 自动通过 __set_name__() 把名字传进去,非常方便且安全。

能力 2:支持多个cached_property实例在一个类中使用,每个都知道自己的名字

假设有以下一个类:

class User:
    @cached_property
    def profile(self): ...

    @cached_property
    def permissions(self): ...

这时 Python 会自动调用 __set_name__() 两次

绑定 profile → attrname = "profile"
绑定 permissions → attrname = "permissions"

每个 cached_property 实例都知道它叫啥,它不会混淆。也就是说:

如果没有这个能力会怎么样?

打个比方,cached_property 就像快递员,快递员一上岗(类定义时),老板(Python 解释器)告诉他:“你负责投递 result”,他以后就知道要把东西塞进instance.__dict__["result"],而不是瞎塞,每个快递员有自己对应的“地址标签”,不会互相搞错。

能力举例通俗比喻
动态识别属性名自动绑定 方法Python 帮你贴了“标签”
多实例支持类中定义多个 @cached_property每个快递员有独立投递地址

🔒 安全保护机制

在标准库实现中,还有如下防御逻辑:

elif name != self.attrname:
	raise TypeError(
	     "Cannot assign the same cached_property to two different names "
	     f"({self.attrname!r} and {name!r})."
	 )

防止将同一个 cached_property 实例赋值到多个属性名:

shared = cached_property(lambda self: 123)

class Foo:
    a = shared
    b = shared  # ❌ 抛出异常

执行时会触发:

TypeError: Cannot assign the same cached_property to two different names ('a' and 'b')

为什么会报错?因为 __set_name__() 是在类定义时自动调用的:

def __set_name__(self, owner, name):
    if self.attrname is None:
        self.attrname = name  # 第一次绑定成功
    elif name != self.attrname:
        raise TypeError(...)  # 第二次绑定,不一致就报错

为什么不能这么做?因为cached_property 的核心机制是:

instance.__dict__[self.attrname] = self.func(instance)

如果 self.attrname 是模糊不定的、反复变,那它根本不知道应该往哪里缓存结果 —— 会导致缓存覆盖、冲突、错乱。

正确做法:为每个属性写一个新的实例

class Foo:
    @cached_property
    def a(self):
        return 1

    @cached_property
    def b(self):
        return 2

每次 @cached_property 都会创建一个新的实例,这就没问题了。

总结

装饰器是否只读是否缓存是否可写是否可删除用途
@property普通只读属性(动态计算)
@x.setter提供 setter 逻辑
@x.deleter提供属性删除逻辑
@cached_property✅ (用 del)高成本、只读缓存型属性(如加载模型)

Python 的属性系统远比它表面上的 obj.x 更加强大。@property 提供了优雅的接口封装,@setter/@deleter 实现了对属性的写删控制,而 @cached_property 则将惰性计算与缓存机制优雅地融合到属性系统之中。

到此这篇关于Python中@property与@cached_property的实现的文章就介绍到这了,更多相关Python @property @cached_property内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文