Python中@property與@cached_property的實現
在 Python 中,屬性不僅僅是簡單的數據字段,它們還可以是動態(tài)計算的值,并且可以封裝讀寫邏輯、懶加載、緩存控制,甚至權限檢查。本文將探討 Python 中與屬性控制相關的四個關鍵裝飾器:
- @property
- @x.setter
- @x.deleter
- @cached_property
一、@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
上述代碼中,radius 和 area 都是方法,但通過 @property 裝飾后,可以像普通屬性一樣使用:
c = Circle(5) print(c.radius) # 5 print(c.area) # 78.54
優(yōu)勢
| 方法調用形式 | 屬性訪問形式 | 好處 |
|---|---|---|
| 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) # 第一次:執(zhí)行計算
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實現了第一次調用時執(zhí)行函數,并將結果存入 __dict__,之后每次訪問直接從緩存中獲取。
然而,筆者在debug中發(fā)現self.attrname會變成函數名,這是為什么呢?
原來從 Python 3.6 開始,描述符類可以實現 __set_name__(self, owner, name) 方法,這個方法在類定義時被自動調用,用于告訴描述符它被賦值到哪個類屬性上。因為 cached_property 是一個描述符,并實現了 set_name 方法。
__set_name__(self, owner, name) 中的參數:
- owner: 擁有這個屬性的類(類本身)
- name: 被綁定到類上的屬性名(字符串)
當 @cached_property 被裝飾函數時(如 @cached_property def result(self): …),
Python 會在類創(chuàng)建期間(class 定義執(zhí)行時)自動調用這個 __set_name__,把方法名(如 “result”)作為 name 參數傳進來,從而完成 self.attrname = “result” 的綁定。
為什么這么設計?
這為 cached_property 提供了兩個關鍵能力:
- 動態(tài)識別屬性名,避免手動硬編碼;
- 支持多個 cached_property 實例在一個類中使用,每個都知道自己的名字。
能力 1:動態(tài)識別屬性名 ——避免手動寫死名字
當我們用 @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 實例都知道它叫啥,它不會混淆。也就是說:
self.attrname = "profile"存 profile 的結果在__dict__["profile"]self.attrname = "permissions"存 permissions 的結果在__dict__["permissions"]
如果沒有這個能力會怎么樣?
- 我們就必須手動傳名字(見上面示例)
- 容易寫錯,比如兩個地方都寫
cached_property("result", ...) - 系統(tǒng)就會誤把
profile的緩存存在了result里
打個比方,cached_property 就像快遞員,快遞員一上崗(類定義時),老板(Python 解釋器)告訴他:“你負責投遞 result”,他以后就知道要把東西塞進instance.__dict__["result"],而不是瞎塞,每個快遞員有自己對應的“地址標簽”,不會互相搞錯。
| 能力 | 舉例 | 通俗比喻 |
|---|---|---|
| 動態(tài)識別屬性名 | 自動綁定 方法 | 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 # ? 拋出異常
執(zhí)行時會觸發(fā):
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 都會創(chuàng)建一個新的實例,這就沒問題了。
總結
| 裝飾器 | 是否只讀 | 是否緩存 | 是否可寫 | 是否可刪除 | 用途 |
|---|---|---|---|---|---|
| @property | ? | ? | ? | ? | 普通只讀屬性(動態(tài)計算) |
| @x.setter | ? | ? | ? | ? | 提供 setter 邏輯 |
| @x.deleter | ? | ? | ? | ? | 提供屬性刪除邏輯 |
| @cached_property | ? | ? | ? | ? (用 del) | 高成本、只讀緩存型屬性(如加載模型) |
Python 的屬性系統(tǒng)遠比它表面上的 obj.x 更加強大。@property 提供了優(yōu)雅的接口封裝,@setter/@deleter 實現了對屬性的寫刪控制,而 @cached_property 則將惰性計算與緩存機制優(yōu)雅地融合到屬性系統(tǒng)之中。
到此這篇關于Python中@property與@cached_property的實現的文章就介紹到這了,更多相關Python @property @cached_property內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Python3 numpy中mean和average的區(qū)別詳解
今天小編就為大家分享一篇在Python3 numpy中mean和average的區(qū)別詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08
Python collections中的雙向隊列deque簡單介紹詳解
這篇文章主要介紹了Python collections中的雙向隊列deque簡單介紹詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11
python?sklearn與pandas實現缺失值數據預處理流程詳解
對于缺失值的處理,主要配合使用sklearn.impute中的SimpleImputer類、pandas、numpy。其中由于pandas對于數據探索、分析和探查的支持較為良好,因此圍繞pandas的缺失值處理較為常用2022-09-09

