Python中@property與@cached_property的實(shí)現(xiàn)
在 Python 中,屬性不僅僅是簡(jiǎn)單的數(shù)據(jù)字段,它們還可以是動(dòng)態(tài)計(jì)算的值,并且可以封裝讀寫邏輯、懶加載、緩存控制,甚至權(quán)限檢查。本文將探討 Python 中與屬性控制相關(guān)的四個(gè)關(guān)鍵裝飾器:
- @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)勢(shì)
方法調(diào)用形式 | 屬性訪問形式 | 好處 |
---|---|---|
obj.get_value() | obj.value | 語義清晰、接口簡(jiǎn)潔 |
二、@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
允許我們對(duì) name
屬性進(jìn)行賦值時(shí)添加校驗(yàn)邏輯:
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 # 調(diào)用自定義的刪除邏輯
三、@cached_property—— 懶加載 + 緩存
在處理高成本計(jì)算時(shí),我們通常希望第一次調(diào)用時(shí)計(jì)算,之后讀取緩存結(jié)果。這正是 @cached_property
的應(yīng)用場(chǎng)景。
示例:懶加載計(jì)算屬性
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í)行計(jì)算 print(obj.result) # 第二次:返回緩存結(jié)果
四、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實(shí)現(xiàn)了第一次調(diào)用時(shí)執(zhí)行函數(shù),并將結(jié)果存入 __dict__
,之后每次訪問直接從緩存中獲取。
然而,筆者在debug中發(fā)現(xiàn)self.attrname會(huì)變成函數(shù)名,這是為什么呢?
原來從 Python 3.6 開始,描述符類可以實(shí)現(xiàn) __set_name__(self, owner, name)
方法,這個(gè)方法在類定義時(shí)被自動(dòng)調(diào)用,用于告訴描述符它被賦值到哪個(gè)類屬性上。因?yàn)?cached_property 是一個(gè)描述符,并實(shí)現(xiàn)了 set_name 方法。
__set_name__(self, owner, name) 中的參數(shù):
- owner: 擁有這個(gè)屬性的類(類本身)
- name: 被綁定到類上的屬性名(字符串)
當(dāng) @cached_property 被裝飾函數(shù)時(shí)(如 @cached_property def result(self): …),
Python 會(huì)在類創(chuàng)建期間(class 定義執(zhí)行時(shí))自動(dòng)調(diào)用這個(gè) __set_name__,把方法名(如 “result”)作為 name 參數(shù)傳進(jìn)來,從而完成 self.attrname = “result” 的綁定。
為什么這么設(shè)計(jì)?
這為 cached_property
提供了兩個(gè)關(guān)鍵能力:
- 動(dòng)態(tài)識(shí)別屬性名,避免手動(dòng)硬編碼;
- 支持多個(gè) cached_property 實(shí)例在一個(gè)類中使用,每個(gè)都知道自己的名字。
能力 1:動(dòng)態(tài)識(shí)別屬性名 ——避免手動(dòng)寫死名字
當(dāng)我們用 @cached_property
裝飾一個(gè)方法時(shí),Python 自動(dòng)把這個(gè)方法的名字(屬性名)告訴它,讓它知道“我是綁定在 xxx
上的”。
就像別人告訴你:“你現(xiàn)在的代號(hào)叫 result,記住它。”
這樣一來,cached_property
就知道以后把緩存結(jié)果存在 obj.__dict__["result"]
里,而不是自己去寫死 'result'
這個(gè)字符串。
如果沒有這個(gè)能力,我們需要手動(dòng)這樣寫:
class cached_property: def __init__(self, name, func): self.func = func self.attrname = name # 必須手動(dòng)傳入 ...
然后使用時(shí)也得這么寫:
class MyClass: result = cached_property("result", lambda self: 42)
不僅麻煩,而且容易出錯(cuò)(比如名字不一致),所以 Python 自動(dòng)通過 __set_name__()
把名字傳進(jìn)去,非常方便且安全。
能力 2:支持多個(gè)cached_property實(shí)例在一個(gè)類中使用,每個(gè)都知道自己的名字
假設(shè)有以下一個(gè)類:
class User: @cached_property def profile(self): ... @cached_property def permissions(self): ...
這時(shí) Python 會(huì)自動(dòng)調(diào)用 __set_name__()
兩次:
綁定 profile → attrname = "profile"
綁定 permissions → attrname = "permissions"
每個(gè) cached_property 實(shí)例都知道它叫啥,它不會(huì)混淆。也就是說:
self.attrname = "profile"
存 profile 的結(jié)果在__dict__["profile"]
self.attrname = "permissions"
存 permissions 的結(jié)果在__dict__["permissions"]
如果沒有這個(gè)能力會(huì)怎么樣?
- 我們就必須手動(dòng)傳名字(見上面示例)
- 容易寫錯(cuò),比如兩個(gè)地方都寫
cached_property("result", ...)
- 系統(tǒng)就會(huì)誤把
profile
的緩存存在了result
里
打個(gè)比方,cached_property
就像快遞員,快遞員一上崗(類定義時(shí)),老板(Python 解釋器)告訴他:“你負(fù)責(zé)投遞 result
”,他以后就知道要把東西塞進(jìn)instance.__dict__["result"]
,而不是瞎塞,每個(gè)快遞員有自己對(duì)應(yīng)的“地址標(biāo)簽”,不會(huì)互相搞錯(cuò)。
能力 | 舉例 | 通俗比喻 |
---|---|---|
動(dòng)態(tài)識(shí)別屬性名 | 自動(dòng)綁定 方法 | Python 幫你貼了“標(biāo)簽” |
多實(shí)例支持 | 類中定義多個(gè) @cached_property | 每個(gè)快遞員有獨(dú)立投遞地址 |
?? 安全保護(hù)機(jī)制
在標(biāo)準(zhǔn)庫實(shí)現(xiàn)中,還有如下防御邏輯:
elif name != self.attrname: raise TypeError( "Cannot assign the same cached_property to two different names " f"({self.attrname!r} and {name!r})." )
防止將同一個(gè) cached_property
實(shí)例賦值到多個(gè)屬性名:
shared = cached_property(lambda self: 123) class Foo: a = shared b = shared # ? 拋出異常
執(zhí)行時(shí)會(huì)觸發(fā):
TypeError: Cannot assign the same cached_property to two different names ('a' and 'b')
為什么會(huì)報(bào)錯(cuò)?因?yàn)?__set_name__() 是在類定義時(shí)自動(dòng)調(diào)用的:
def __set_name__(self, owner, name): if self.attrname is None: self.attrname = name # 第一次綁定成功 elif name != self.attrname: raise TypeError(...) # 第二次綁定,不一致就報(bào)錯(cuò)
為什么不能這么做?因?yàn)閏ached_property 的核心機(jī)制是:
instance.__dict__[self.attrname] = self.func(instance)
如果 self.attrname 是模糊不定的、反復(fù)變,那它根本不知道應(yīng)該往哪里緩存結(jié)果 —— 會(huì)導(dǎo)致緩存覆蓋、沖突、錯(cuò)亂。
正確做法:為每個(gè)屬性寫一個(gè)新的實(shí)例
class Foo: @cached_property def a(self): return 1 @cached_property def b(self): return 2
每次 @cached_property 都會(huì)創(chuàng)建一個(gè)新的實(shí)例,這就沒問題了。
總結(jié)
裝飾器 | 是否只讀 | 是否緩存 | 是否可寫 | 是否可刪除 | 用途 |
---|---|---|---|---|---|
@property | ? | ? | ? | ? | 普通只讀屬性(動(dòng)態(tài)計(jì)算) |
@x.setter | ? | ? | ? | ? | 提供 setter 邏輯 |
@x.deleter | ? | ? | ? | ? | 提供屬性刪除邏輯 |
@cached_property | ? | ? | ? | ? (用 del) | 高成本、只讀緩存型屬性(如加載模型) |
Python 的屬性系統(tǒng)遠(yuǎn)比它表面上的 obj.x
更加強(qiáng)大。@property
提供了優(yōu)雅的接口封裝,@setter/@deleter
實(shí)現(xiàn)了對(duì)屬性的寫刪控制,而 @cached_property
則將惰性計(jì)算與緩存機(jī)制優(yōu)雅地融合到屬性系統(tǒng)之中。
到此這篇關(guān)于Python中@property與@cached_property的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Python @property @cached_property內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在Python3 numpy中mean和average的區(qū)別詳解
今天小編就為大家分享一篇在Python3 numpy中mean和average的區(qū)別詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-08-08python使用jieba實(shí)現(xiàn)中文分詞去停用詞方法示例
jieba分詞,完全開源,有集成的python庫,簡(jiǎn)單易用。下面這篇文章主要給大家介紹了關(guān)于python使用jieba實(shí)現(xiàn)中文分詞去停用詞的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2018-03-03Python collections中的雙向隊(duì)列deque簡(jiǎn)單介紹詳解
這篇文章主要介紹了Python collections中的雙向隊(duì)列deque簡(jiǎn)單介紹詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Python 無限級(jí)分類樹狀結(jié)構(gòu)生成算法的實(shí)現(xiàn)
這篇文章主要介紹了Python 無限級(jí)分類樹狀結(jié)構(gòu)生成算法的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01python?sklearn與pandas實(shí)現(xiàn)缺失值數(shù)據(jù)預(yù)處理流程詳解
對(duì)于缺失值的處理,主要配合使用sklearn.impute中的SimpleImputer類、pandas、numpy。其中由于pandas對(duì)于數(shù)據(jù)探索、分析和探查的支持較為良好,因此圍繞pandas的缺失值處理較為常用2022-09-09