Python 的 property():向類添加托管屬性
目錄

管理類中的屬性
Python 中的 Getter 和 Setter 方法
Pythonic 方法
Python 的 property() 入門
使用 property() 創建屬性
使用 property() 作為裝飾器
提供只讀屬性
創建讀寫屬性
提供只寫屬性
將 Python 的 property() 付諸行動
驗證輸入值
提供計算屬性
緩存計算屬性
記錄屬性訪問和變異
管理屬性刪除
創建向后兼容的類 API
覆蓋子類中的屬性
結論
使用 Python 的property(),您可以在類中創建托管屬性。當您需要修改其內部實現而不更改類的公共API時,您可以使用托管屬性,也稱為屬性。提供穩定的 API 可以幫助您避免在用戶依賴您的類和對象時破壞他們的代碼。
屬性可以說是最流行的以最純粹的Pythonic風格快速創建托管屬性的方式。
在本教程中,您將學習如何:
在您的類中創建托管屬性或屬性
執行惰性屬性評估并提供計算屬性
避免使用setter和getter方法讓你的類更 Pythonic
創建read-only、read-write和write-only屬性
為您的類創建一致且向后兼容的 API
您還將編寫一些property()用于驗證輸入數據、動態計算屬性值、記錄代碼等的實際示例。為了充分利用本教程,您應該了解Python 中面向對象編程和裝飾器的基礎知識。
管理類中的屬性
當您在面向對象的編程語言中定義一個類時,您可能最終會得到一些實例和類屬性。換句話說,您最終會得到可通過實例、類或什至兩者訪問的變量,具體取決于語言。屬性代表或保存給定對象的內部狀態,您經常需要訪問和改變它。
通常,您至少有兩種方法來管理屬性。您可以直接訪問和改變屬性,也可以使用methods。方法是附加到給定類的函數。它們提供對象可以使用其內部數據和屬性執行的行為和操作。
如果您向用戶公開您的屬性,那么它們將成為您類的公共API的一部分。您的用戶將直接在他們的代碼中訪問和改變它們。當您需要更改給定屬性的內部實現時,就會出現問題。
假設您正在Circle上課。最初的實現有一個名為.radius.?您完成了該類的編碼并將其提供給您的最終用戶。他們開始Circle在他們的代碼中使用來創建許多很棒的項目和應用程序。做得好!
現在假設您有一個重要用戶向您提出新要求。他們不想Circle再存儲半徑。他們需要一個公共.diameter屬性。
此時,刪除.radius以開始使用.diameter可能會破壞某些最終用戶的代碼。您需要以除刪除.radius.
Java和C++等編程語言鼓勵您永遠不要公開您的屬性以避免此類問題。相反,您應該提供getter和setter方法,也分別稱為accessors和mutators。這些方法提供了一種無需更改公共 API 即可更改屬性的內部實現的方法。
注意:?Getter 和 setter 方法通常被認為是一種反模式和面向對象設計不佳的信號。這個命題背后的主要論點是這些方法打破了封裝。它們允許您訪問和更改對象的組件。
最后,這些語言需要 getter 和 setter 方法,因為如果給定的需求發生變化,它們沒有提供合適的方法來更改屬性的內部實現。更改內部實現需要修改 API,這可能會破壞最終用戶的代碼。
Python 中的 Getter 和 Setter 方法
從技術上講,沒有什么可以阻止您在 Python 中使用 getter 和 setter方法。以下是這種方法的外觀:
# point.py class Point: def __init__(self, x, y): self._x = x self._y = y def get_x(self): return self._x def set_x(self, value): self._x = value def get_y(self): return self._y def set_y(self, value): self._y = value
在此示例中,您Point使用兩個非公共屬性?創建._x并._y保存手頭點的笛卡爾坐標。
注:?Python沒有概念訪問修飾符,如private,protected和public,限制訪問的屬性和方法。在 Python 中,區別在于公共和非公共類成員。
如果要表示給定的屬性或方法是非公開的,則必須使用眾所周知的 Python約定,即在名稱前加上下劃線 (?_)。這就是命名屬性._x和._y.
請注意,這只是一個約定。它不會阻止您和其他程序員使用點表示法訪問屬性,如obj._attr.?但是,違反此約定是不好的做法。
要訪問和改變._xor的值._y,您可以使用相應的 getter 和 setter 方法。來吧,保存的上述定義Point一個Python的模塊和導入類到你的交互shell。
以下是您可以Point在代碼中使用的方法:
>>>
>>> from point import Point >>> point = Point(12, 5) >>> point.get_x() 12 >>> point.get_y() 5 >>> point.set_x(42) >>> point.get_x() 42 >>> # Non-public attributes are still accessible >>> point._x 42 >>> point._y 5
使用.get_x()and?.get_y(),您可以訪問._xand的當前值._y。您可以使用 setter 方法在相應的托管屬性中存儲新值。從這段代碼中,您可以確認 Python 不限制對非公共屬性的訪問。是否這樣做取決于您。
Pythonic 方法
盡管您剛剛看到的示例使用了 Python 編碼風格,但它看起來并不像 Pythonic。在該示例中,getter 和 setter 方法不使用._x和執行任何進一步處理._y。你可以Point用更簡潔和 Pythonic 的方式重寫:
>>>
>>> class Point: ... def __init__(self, x, y): ... self.x = x ... self.y = y ... >>> point = Point(12, 5) >>> point.x 12 >>> point.y 5 >>> point.x = 42 >>> point.x 42
這段代碼揭示了一個基本原則。在 Python 中,向最終用戶公開屬性是正常和常見的。你不需要一直用 getter 和 setter 方法來弄亂你的類,這聽起來很酷!但是,您如何處理似乎涉及 API 更改的需求更改?
與 Java 和 C++ 不同,Python 提供了方便的工具,允許您在不更改公共 API 的情況下更改屬性的底層實現。最流行的方法是將您的屬性轉換為屬性。
注意:提供托管屬性的另一種常見方法是使用描述符。但是,在本教程中,您將了解屬性。
屬性表示普通屬性(或字段)和方法之間的中間功能。換句話說,它們允許您創建行為類似于屬性的方法。使用屬性,您可以在需要時更改計算目標屬性的方式。
例如,您可以將.x和 都.y轉換為屬性。通過此更改,您可以繼續將它們作為屬性進行訪問。您還將擁有一個底層方法.x,.y這將允許您在用戶訪問和改變它們之前修改它們的內部實現并對它們執行操作。
注意:屬性不是 Python 獨有的。語言作為這樣的JavaScript,C#?,科特林,和其他人還提供了工具和技術來創建屬性類成員。
Python 屬性的主要優點是它們允許您將屬性作為公共 API 的一部分公開。如果您需要更改底層實現,那么您可以隨時將屬性轉換為屬性,而不會有太多痛苦。
在以下部分中,您將學習如何在 Python 中創建屬性。
Python 入門?property()
Pythonproperty()是避免在代碼中使用正式的 getter 和 setter 方法的 Pythonic 方式。此功能允許您將類屬性轉換為屬性或托管屬性。由于property()是內置函數,因此您無需導入任何內容即可使用它。此外,在 Cproperty()中實現以確保最佳性能。
注意:通常property()稱為內置函數。但是,property是一個旨在作為函數而不是常規類工作的類。這就是為什么大多數 Python 開發人員稱其為函數的原因。這也是為什么property()不遵循 Python命名 classes約定的原因。
本教程遵循調用property()函數而不是類的常見做法。但是,在某些部分中,您會看到它被稱為類以方便解釋。
使用property(),您可以將 getter 和 setter 方法附加到給定的類屬性。這樣,您就可以處理該屬性的內部實現,而無需在 API 中公開 getter 和 setter 方法。您還可以指定處理屬性刪除的方法并為您的屬性提供適當的文檔字符串。
這是 的完整簽名property():
property(fget=None, fset=None, fdel=None, doc=None)
前兩個參數采用函數對象,它們將扮演 getter (?fget) 和 setter (?fset) 方法的角色。下面總結了每個參數的作用:
的返回值property()是托管屬性本身。如果您訪問托管屬性(如 )obj.attr,則 Python 會自動調用fget().?如果您為屬性分配一個新值(如 )obj.attr = value,則 Pythonfset()使用輸入value作為參數進行調用。最后,如果你運行一個del obj.attr語句,那么 Python 會自動調用fdel().
注意:property()取函數對象的前三個參數。您可以將函數對象視為沒有調用括號對的函數名稱。
您可以使用doc為您的屬性提供適當的文檔字符串。您和您的其他程序員將能夠使用 Python 的help().?doc當您使用支持文檔字符串訪問的代碼編輯器和 IDE 時,該參數也很有用。
您可以將property()其用作函數或裝飾器來構建屬性。在以下兩節中,您將學習如何使用這兩種方法。但是,您應該事先知道裝飾器方法在 Python 社區中更受歡迎。
創建屬性?property()
您可以通過property()使用一組適當的參數調用并將其返回值分配給類屬性來創建屬性。的所有參數property()都是可選的。但是,您通常至少提供一個setter function。
以下示例顯示了如何創建一個Circle具有方便屬性的類來管理其半徑:
# circle.py class Circle: def __init__(self, radius): self._radius = radius def _get_radius(self): print("Get radius") return self._radius def _set_radius(self, value): print("Set radius") self._radius = value def _del_radius(self): print("Delete radius") del self._radius radius = property( fget=_get_radius, fset=_set_radius, fdel=_del_radius, doc="The radius property." )
在此代碼片段中,您創建Circle.?類初始值設定項.__init__()將其radius作為參數并將其存儲在名為 的非公共屬性中._radius。然后定義三個非公共方法:
._get_radius()?返回當前值?._radius
._set_radius()將其value作為參數并將其分配給._radius
._del_radius()?刪除實例屬性?._radius
一旦你有了這三個方法,你就可以創建一個類屬性,調用它.radius來存儲屬性對象。要初始化該屬性,請將三個方法作為參數傳遞給property()。您還可以為您的財產傳遞一個合適的文檔字符串。
在此示例中,您使用關鍵字參數來提高代碼可讀性并防止混淆。這樣,您就可以確切地知道每個參數使用哪個方法。
要Circle試一試,請在您的 Python shell 中運行以下代碼:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius Get radius 42.0 >>> circle.radius = 100.0 Set radius >>> circle.radius Get radius 100.0 >>> del circle.radius Delete radius >>> circle.radius Get radius Traceback (most recent call last): ... AttributeError: 'Circle' object has no attribute '_radius' >>> help(circle) Help on Circle in module __main__ object: class Circle(builtins.object) ... | radius | The radius property.
該.radius屬性隱藏了非公共實例屬性._radius,它現在是您在本示例中的托管屬性。您可以.radius直接訪問和分配。計算機內部,Python會自動調用._get_radius()并._set_radius()在需要的時候。當您執行時del circle.radius,Python 會調用._del_radius(),這會刪除底層的._radius.
使用lambda函數作為 Getter 方法顯示隱藏
屬性是管理實例屬性的類屬性。您可以將屬性視為捆綁在一起的方法的集合。如果您檢查.radius仔細,那么你就可以找到你的所提供的原始方法fget,fset以及fdel參數:
>>>
>>> from circle import Circle >>> Circle.radius.fget
您可以通過相應的訪問的getter,setter方法,并刪除器在給定的屬性方法.fget,.fset和.fdel。
屬性也是覆蓋描述符。如果您使用dir()檢查給定屬性的內部成員,那么您將在列表中找到.__set__()和.__get__()。這些方法提供了描述符協議的默認實現。
注意:如果你想更好地理解propertyas 類的內部實現,那么查看文檔中描述的純 PythonProperty類。
.__set__()例如,的默認實現在您不提供自定義 setter 方法時運行。在這種情況下,您會得到一個,AttributeError因為無法設置基礎屬性。
使用property()作為裝飾
裝飾器在 Python 中無處不在。它們是將另一個函數作為參數并返回具有附加功能的新函數的函數。使用裝飾器,您可以將預處理和后處理操作附加到現有函數。
當Python 2.2引入時property(),裝飾器語法不可用。正如您之前學到的,定義屬性的唯一方法是傳遞 getter、setter 和 deleter 方法。裝飾器語法是在Python 2.4中添加的,現在,property()作為裝飾器使用是 Python 社區中最流行的做法。
裝飾器語法包括在要裝飾的函數@定義之前放置帶有前導符號的裝飾器函數的名稱:
@decorator def func(a): return a
在這個代碼片段中,@decorator可以是一個函數或類來裝飾func().?此語法等效于以下內容:
def func(a): return a func = decorator(func)
The final line of code reassigns the name?func?to hold the result of calling?decorator(func). Note that this is the same syntax you used to create a property in the section above.
Python’s?property()?can also work as a decorator, so you can use the?@property?syntax to create your properties quickly:
1# circle.py 2 3class Circle: 4 def __init__(self, radius): 5 self._radius = radius 6 7 @property 8 def radius(self): 9 """The radius property.""" 10 print("Get radius") 11 return self._radius 12 13 @radius.setter 14 def radius(self, value): 15 print("Set radius") 16 self._radius = value 17 18 @radius.deleter 19 def radius(self): 20 print("Delete radius") 21 del self._radius
This code looks pretty different from the getter and setter methods approach.?Circle?now looks more Pythonic and clean. You don’t need to use method names such as?._get_radius(),?._set_radius(), and?._del_radius()?anymore. Now you have three methods with the same clean and descriptive attribute-like name. How is that possible?
用于創建屬性的裝飾器方法需要使用底層托管屬性的公共名稱定義第一個方法,.radius在本例中就是這樣。此方法應實現 getter 邏輯。在上面的示例中,第 7 行到第 11 行實現了該方法。
第 13 到 16 行定義了 的 setter 方法.radius。在這種情況下,語法完全不同。您無需@property再次使用,而是使用@radius.setter.?為什么你需要這樣做?再看一下dir()輸出:
>>>
>>> dir(Circle.radius) [..., 'deleter', ..., 'getter', 'setter']
此外.fget,.fset,.fdel,和一堆其他特殊的屬性和方法,property還提供了.deleter(),.getter()和.setter()。這三個方法各自返回一個新屬性。
當您.radius()使用@radius.setter(第 13 行)裝飾第二個方法時,您創建了一個新屬性并重新分配了類級名稱.radius(第 8 行)來保存它。這個新屬性包含第 8 行初始屬性的相同方法集,并添加了第 14 行提供的新 setter 方法。最后,裝飾器語法將新屬性重新分配給.radius類級名稱。
定義刪除器方法的機制是類似的。這一次,您需要使用@radius.deleter裝飾器。在該過程結束時,您將獲得具有 getter、setter 和 deleter 方法的完整屬性。
最后,當您使用裝飾器方法時,如何為您的屬性提供合適的文檔字符串?如果您Circle再次檢查,您會注意到您已經在第 9 行的 getter 方法中添加了一個文檔字符串。
新Circle實現的工作方式與上一節中的示例相同:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius Get radius 42.0 >>> circle.radius = 100.0 Set radius >>> circle.radius Get radius 100.0 >>> del circle.radius Delete radius >>> circle.radius Get radius Traceback (most recent call last): ... AttributeError: 'Circle' object has no attribute '_radius' >>> help(circle) Help on Circle in module __main__ object: class Circle(builtins.object) ... | radius | The radius property.
您不需要使用一對括號.radius()作為方法調用。相反,您可以.radius像訪問常規屬性一樣訪問,這是屬性的主要用途。它們允許您將方法視為屬性,并負責自動調用底層方法集。
以下是使用裝飾器方法創建屬性時要記住的一些要點:
在@property裝飾裝修必須的getter方法。
文檔字符串必須在getter 方法中。
該setter和刪除器方法必須與getter方法加上的名字裝飾.setter和.getter分別。
到目前為止,您已經創建了property()用作函數和裝飾器的托管屬性。如果您檢查Circle到目前為止的實現,那么您會注意到它們的 getter 和 setter 方法不會在您的屬性之上添加任何真正的額外處理。
通常,您應該避免將不需要額外處理的屬性轉換為屬性。在這些情況下使用屬性可以使您的代碼:
不必要的冗長
混淆其他開發者
比基于常規屬性的代碼慢
除非您需要的不僅僅是訪問裸屬性,否則不要編寫屬性。它們是在浪費CPU時間,更重要的是,它們是在浪費您的時間。最后,您應該避免編寫顯式的 getter 和 setter 方法,然后將它們包裝在一個屬性中。相反,使用@property裝飾器。這是目前最 Pythonic 的方式。
提供只讀屬性
可能最基本的用例property()是在您的類中提供只讀屬性。假設您需要一個不可變?Point類,它不允許用戶改變其坐標的原始值,x并且y.?為了實現這個目標,你可以Point像下面的例子一樣創建:
# point.py class Point: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @property def y(self): return self._y
在這里,您將輸入參數存儲在屬性._x和 中._y。正如您已經了解到的,_在名稱中使用前導下劃線 (?) 告訴其他開發人員它們是非公共屬性,不應使用點表示法訪問,例如在point._x.?最后,定義兩個 getter 方法并用@property.
現在您有兩個只讀屬性,.x和.y作為您的坐標:
>>>
>>> from point import Point >>> point = Point(12, 5) >>> # Read coordinates >>> point.x 12 >>> point.y 5 >>> # Write coordinates >>> point.x = 42 Traceback (most recent call last): ... AttributeError: can't set attribute
此處point.x和point.y是只讀屬性的基本示例。它們的行為依賴于property提供的底層描述符。正如您已經看到的,當您沒有定義正確的 setter 方法時,默認.__set__()實現會引發 an?AttributeError。
您可以Point更進一步地實現此實現,并提供顯式的 setter 方法,這些方法會引發帶有更詳細和特定消息的自定義異常:
# point.py class WriteCoordinateError(Exception): pass class Point: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @x.setter def x(self, value): raise WriteCoordinateError("x coordinate is read-only") @property def y(self): return self._y @y.setter def y(self, value): raise WriteCoordinateError("y coordinate is read-only")
在此示例中,您定義了一個名為 的自定義異常WriteCoordinateError。此異常允許您自定義實現不可變Point類的方式。現在,這兩個 setter 方法都會通過更明確的消息引發您的自定義異常。來Point試試你的改進吧!
創建讀寫屬性
您還可以使用property()提供具有讀寫功能的托管屬性。在實踐中,您只需要為您的屬性提供適當的 getter 方法(“read”)和 setter 方法(“write”)即可創建讀寫托管屬性。
假設你希望你的Circle類有一個.diameter屬性。但是,在類初始值設定項中獲取半徑和直徑似乎沒有必要,因為您可以使用另一個來計算一個。這是一個Circle管理.radius和.diameter作為讀寫屬性的:
# circle.py import math class Circle: def __init__(self, radius): self.radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = float(value) @property def diameter(self): return self.radius * 2 @diameter.setter def diameter(self, value): self.radius = value / 2
在這里,您創建一個Circle具有讀寫權限的類.radius。在這種情況下,getter 方法只返回半徑值。setter 方法轉換半徑的輸入值并將其分配給 non-public?._radius,這是您用來存儲最終數據的變量。
在這個Circle和它的.radius屬性的新實現中有一個微妙的細節需要注意。在這種情況下,類初始值設定項.radius直接將輸入值分配給屬性,而不是將其存儲在專用的非公共屬性中,例如._radius.
為什么?因為您需要確保作為半徑提供的每個值,包括初始化值,都通過 setter 方法并轉換為浮點數。
Circle還將.diameter屬性實現為屬性。getter 方法使用半徑計算直徑。setter 方法做了一些奇怪的事情。它不是將輸入直徑存儲value在專用屬性中,而是計算半徑并將結果寫入.radius.
以下是您的Circle工作方式:
>>>
>>> from circle import Circle >>> circle = Circle(42) >>> circle.radius 42.0 >>> circle.diameter 84.0 >>> circle.diameter = 100 >>> circle.diameter 100.0 >>> circle.radius 50.0
.radius和.diameter在這些示例中都作為普通屬性工作,為您的Circle類提供干凈和 Pythonic 的公共 API?。
提供只寫屬性
您還可以通過調整實現屬性的 getter 方法的方式來創建只寫屬性。例如,您可以讓您的 getter 方法在用戶每次訪問基礎屬性值時引發異常。
以下是使用只寫屬性處理密碼的示例:
# users.py import hashlib import os class User: def __init__(self, name, password): self.name = name self.password = password @property def password(self): raise AttributeError("Password is write-only") @password.setter def password(self, plaintext): salt = os.urandom(32) self._hashed_password = hashlib.pbkdf2_hmac( "sha256", plaintext.encode("utf-8"), salt, 100_000 )
的初始值設定項User將用戶名和密碼作為參數并將它們分別存儲在.name和 中.password。您使用屬性來管理您的類如何處理輸入密碼。AttributeError每當用戶嘗試檢索當前密碼時,getter 方法都會引發。這變成.password了只寫屬性:
>>>
>>> from users import User >>> john = User("John", "secret") >>> john._hashed_password b'b\xc7^ai\x9f3\xd2g ... \x89^-\x92\xbe\xe6' >>> john.password Traceback (most recent call last): ... AttributeError: Password is write-only >>> john.password = "supersecret" >>> john._hashed_password b'\xe9l$\x9f\xaf\x9d ... b\xe8\xc8\xfcaU\r_'
在此示例中,您將創建john一個User具有初始密碼的實例。setter 方法對密碼進行哈希處理并將其存儲在._hashed_password.?請注意,當您嘗試.password直接訪問時,您會得到一個AttributeError.?最后,分配一個新值來.password觸發 setter 方法并創建一個新的散列密碼。
在 setter 方法中.password,您使用os.urandom()生成一個 32 字節的隨機字符串作為散列函數的salt。要生成散列密碼,請使用hashlib.pbkdf2_hmac().?然后將生成的散列密碼存儲在非公共屬性中._hashed_password。這樣做可確保您永遠不會將明文密碼保存在任何可檢索的屬性中。
將 Pythonproperty()付諸行動
到目前為止,您已經學習了如何使用 Python 的property()內置函數在類中創建托管屬性。您用作property()函數和裝飾器,并了解了這兩種方法之間的差異。您還學習了如何創建只讀、讀寫和只寫屬性。
在以下部分中,您將編寫一些示例代碼,以幫助您更好地實際理解property().
驗證輸入值
最常見的用例之一property()是構建托管屬性,在存儲甚至將其作為安全輸入接受之前驗證輸入數據。數據驗證是代碼中的一個常見要求,它接受來自用戶或您認為不可信的其他信息源的輸入。
Pythonproperty()提供了一種快速可靠的工具來處理輸入數據驗證。例如,回想著Point例如,您可能需要的值.x,并.y為有效的數字。由于您的用戶可以自由輸入任何類型的數據,因此您需要確保您的點僅接受數字。
這Point是管理此要求的實現:
# point.py class Point: def __init__(self, x, y): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, value): try: self._x = float(value) print("Validated!") except ValueError: raise ValueError('"x" must be a number') from None @property def y(self): return self._y @y.setter def y(self, value): try: self._y = float(value) print("Validated!") except ValueError: raise ValueError('"y" must be a number') from None
使用 Python?EAFP樣式驗證輸入數據的 setter 方法.x和.y使用try...except塊。如果調用成功,則輸入數據有效,您將進入屏幕。如果引發 a?,則用戶會收到帶有更具體消息的 a 。float()Validated!float()ValueErrorValueError
注意:在上面的示例中,您使用語法raise…from None來隱藏與引發異常的上下文相關的內部詳細信息。從最終用戶的角度來看,這些細節可能會讓人感到困惑,并使您的類看起來很粗糙。
查看文檔中有關raise聲明的部分,了解有關此主題的更多信息。
重要的是要注意,直接分配.x和.y屬性.__init__()可確保在對象初始化期間也進行驗證。不這樣做是property()用于數據驗證時的常見錯誤。
以下是您的Point課程現在的工作方式:
>>>
>>> from point import Point >>> point = Point(12, 5) Validated! Validated! >>> point.x 12.0 >>> point.y 5.0 >>> point.x = 42 Validated! >>> point.x 42.0 >>> point.y = 100.0 Validated! >>> point.y 100.0 >>> point.x = "one" Traceback (most recent call last): ... ValueError: "x" must be a number >>> point.y = "1o" Traceback (most recent call last): ... ValueError: "y" must be a number
如果賦值.x和.y值float()可以變成浮點數,則驗證成功,該值被接受。否則,您將獲得一個ValueError.
這種實現Point揭示了property().?你發現了嗎?
就是這樣!您有遵循特定模式的重復代碼。這種重復違反了DRY(不要重復自己)原則,因此您需要重構此代碼以避免它。為此,您可以使用描述符抽象出重復的邏輯:
# point.py class Coordinate: def __set_name__(self, owner, name): self._name = name def __get__(self, instance, owner): return instance.__dict__[self._name] def __set__(self, instance, value): try: instance.__dict__[self._name] = float(value) print("Validated!") except ValueError: raise ValueError(f'"{self._name}" must be a number') from None class Point: x = Coordinate() y = Coordinate() def __init__(self, x, y): self.x = x self.y = y
現在你的代碼有點短了。您通過定義在一個地方管理數據驗證Coordinate的描述符,設法刪除了重復的代碼。該代碼的工作方式與您之前的實現方式相同。來試試看吧!
通常,如果您發現自己在代碼周圍復制和粘貼屬性定義,或者發現像上面示例中那樣重復的代碼,那么您應該考慮使用適當的描述符。
提供計算屬性
如果您需要一個在訪問時動態構建其值的屬性,那么property()這就是您要走的路。這些類型的屬性通常稱為計算屬性。當您需要它們看起來像熱切的屬性時,它們會很方便,但您希望它們是惰性的。
創建 Eager 屬性的主要原因是在您經常訪問屬性時優化計算成本。另一方面,如果您很少使用給定的屬性,那么惰性屬性可以將其計算推遲到需要時,這可以使您的程序更高效。
以下是如何在類中property()創建計算屬性的示例:.areaRectangle
class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def area(self): return self.width * self.height
在此示例中,Rectangle初始化程序將width和height作為參數并將它們存儲在常規實例屬性中。.area每次訪問時,只讀屬性都會計算并返回當前矩形的面積。
屬性的另一個常見用例是為給定屬性提供自動格式化的值:
class Product: def __init__(self, name, price): self._name = name self._price = float(price) @property def price(self): return f"${self._price:,.2f}"
在此示例中,.price是格式化并返回特定產品價格的屬性。要提供類似貨幣的格式,請使用帶有適當格式選項的f 字符串。
注意:此示例使用浮點數來表示貨幣,這是不好的做法。相反,你應該使用decimal.Decimal從標準庫。
作為計算屬性的最后一個示例,假設您有一個Point使用.x和.y作為笛卡爾坐標的類。您想為您的點提供極坐標,以便您可以在一些計算中使用它們。極坐標系使用到原點的距離和與水平坐標軸的角度來表示每個點。
這是一個笛卡爾坐標Point類,它也提供計算的極坐標:
# point.py import math class Point: def __init__(self, x, y): self.x = x self.y = y @property def distance(self): return round(math.dist((0, 0), (self.x, self.y))) @property def angle(self): return round(math.degrees(math.atan(self.y / self.x)), 1) def as_cartesian(self): return self.x, self.y def as_polar(self): return self.distance, self.angle
此示例說明如何Point使用給定對象.x和.y笛卡爾坐標計算給定對象的距離和角度。下面是這段代碼在實踐中的工作方式:
>>>
>>> from point import Point >>> point = Point(12, 5) >>> point.x 12 >>> point.y 5 >>> point.distance 13 >>> point.angle 22.6 >>> point.as_cartesian() (12, 5) >>> point.as_polar() (13, 22.6)
在提供計算屬性或惰性屬性時,這property()是一個非常方便的工具。但是,如果您要創建一個經常使用的屬性,那么每次都計算它既昂貴又浪費。一個好的策略是在計算完成后緩存它們。
緩存計算屬性
有時您有一個經常使用的給定計算屬性。不斷重復相同的計算可能是不必要和昂貴的。要解決此問題,您可以緩存計算值并將其保存在非公共專用屬性中以供進一步重用。
為了防止意外行為,您需要考慮輸入數據的可變性。如果您有一個根據常量輸入值計算其值的屬性,那么結果將永遠不會改變。在這種情況下,您只需計算一次該值:
# circle.py from time import sleep class Circle: def __init__(self, radius): self.radius = radius self._diameter = None @property def diameter(self): if self._diameter is None: sleep(0.5) # Simulate a costly computation self._diameter = self.radius * 2 return self._diameter
即使這個實現Circle正確緩存了計算出的直徑,它也有一個缺點,如果你改變了 的值.radius,那么.diameter將不會返回正確的值:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 42.0 >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100.0 >>> circle.diameter # Wrong diameter 84.0
在這些示例中,您將創建一個半徑等于 的圓42.0。該.diameter屬性僅在您第一次訪問時計算其值。這就是為什么您在第一次執行中看到延遲而在第二次執行中沒有延遲的原因。請注意,即使您更改了半徑值,直徑也保持不變。
如果計算屬性的輸入數據發生變異,則需要重新計算屬性:
# circle.py from time import sleep class Circle: def __init__(self, radius): self.radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): self._diameter = None self._radius = value @property def diameter(self): if self._diameter is None: sleep(0.5) # Simulate a costly computation self._diameter = self._radius * 2 return self._diameter
每次更改半徑值時,.radius屬性的 setter 方法都會重置._diameter為None。通過這個小更新,.diameter在每次更改 后第一次訪問它時重新計算它的值.radius:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 42.0 >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100.0 >>> circle.diameter # With delay 200.0 >>> circle.diameter # Without delay 200.0
涼爽的!Circle現在工作正常!它會在您第一次訪問它以及每次更改半徑時計算直徑。
創建緩存屬性的另一個選項是functools.cached_property()從標準庫中使用。此函數用作裝飾器,允許您將方法轉換為緩存屬性。該屬性僅計算一次其值,并在實例的生命周期內將其作為普通屬性緩存:
# circle.py from functools import cached_property from time import sleep class Circle: def __init__(self, radius): self.radius = radius @cached_property def diameter(self): sleep(0.5) # Simulate a costly computation return self.radius * 2
在這里,.diameter在您第一次訪問它時計算并緩存它的值。這種實現適用于那些輸入值不會發生變化的計算。這是它的工作原理:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100 >>> circle.diameter # Wrong diameter 84.0 >>> # Allow direct assignment >>> circle.diameter = 200 >>> circle.diameter # Cached value 200
當您訪問 時.diameter,您將獲得它的計算值。從現在開始,該值保持不變。但是,與 不同的是property(),cached_property()除非您提供適當的 setter 方法,否則不會阻止屬性更改。這就是為什么您可以200在最后幾行中將直徑更新為。
如果要創建不允許修改的緩存屬性,則可以在以下示例中使用property()and?functools.cache()like:
# circle.py from functools import cache from time import sleep class Circle: def __init__(self, radius): self.radius = radius @property @cache def diameter(self): sleep(0.5) # Simulate a costly computation return self.radius * 2
此代碼堆疊@property在@cache.?兩個裝飾器的組合構建了一個緩存屬性,以防止突變:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100 >>> circle.diameter 84.0 >>> circle.diameter = 200 Traceback (most recent call last): ... AttributeError: can't set attribute
在這些示例中,當您嘗試為 分配新值時.diameter,您會得到 ,AttributeError因為 setter 功能來自 的內部描述符property。
記錄屬性訪問和變異
有時您需要跟蹤代碼的作用以及程序的運行方式。在 Python 中這樣做的一種方法是使用logging.?該模塊提供了記錄代碼所需的所有功能。它將允許您不斷觀察代碼并生成有關其工作方式的有用信息。
如果您需要跟蹤訪問和更改給定屬性的方式和時間,那么您也可以利用property()它:
# circle.py import logging logging.basicConfig( format="%(asctime)s: %(message)s", level=logging.INFO, datefmt="%H:%M:%S" ) class Circle: def __init__(self, radius): self._msg = '"radius" was %s. Current value: %s' self.radius = radius @property def radius(self): """The radius property.""" logging.info(self._msg % ("accessed", str(self._radius))) return self._radius @radius.setter def radius(self, value): try: self._radius = float(value) logging.info(self._msg % ("mutated", str(self._radius))) except ValueError: logging.info('validation error while mutating "radius"')
在這里,您首先導入logging并定義一個基本配置。然后Circle使用托管屬性實現.radius。每次.radius在代碼中訪問時,getter 方法都會生成日志信息。setter 方法記錄您在 上執行的每個更改.radius。它還記錄由于輸入數據錯誤而出現錯誤的情況。
以下是您可以Circle在代碼中使用的方法:
>>>
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 14:48:59: "radius" was accessed. Current value: 42.0 42.0 >>> circle.radius = 100 14:49:15: "radius" was mutated. Current value: 100 >>> circle.radius 14:49:24: "radius" was accessed. Current value: 100 100 >>> circle.radius = "value" 15:04:51: validation error while mutating "radius"
記錄來自屬性訪問和變異的有用數據可以幫助您調試代碼。日志記錄還可以幫助您識別有問題的數據輸入的來源、分析代碼的性能、發現使用模式等。
管理屬性刪除
您還可以創建實現刪除功能的屬性。這可能是 的罕見用例property(),但在某些情況下,有一種刪除屬性的方法可能會很方便。
假設您正在實現自己的樹數據類型。樹是一種抽象數據類型,它在層次結構中存儲元素。樹組件通常稱為節點。樹中的每個節點都有一個父節點,除了根節點。節點可以有零個或多個子節點。
現在假設您需要提供一種方法來刪除或清除給定節點的子節點列表。這是一個實現樹節點的示例,該節點property()用于提供其大部分功能,包括清除手頭節點的子節點列表的能力:
# tree.py class TreeNode: def __init__(self, data): self._data = data self._children = [] @property def children(self): return self._children @children.setter def children(self, value): if isinstance(value, list): self._children = value else: del self.children self._children.append(value) @children.deleter def children(self): self._children.clear() def __repr__(self): return f'{self.__class__.__name__}("{self._data}")'
在此示例中,TreeNode表示自定義樹數據類型中的一個節點。每個節點將其子節點存儲在 Python列表中。然后您將其實現.children為一個屬性來管理子項的基礎列表。deleter 方法調用.clear()子項列表以將它們全部刪除:
>>>
>>> from tree import TreeNode >>> root = TreeNode("root") >>> child1 = TreeNode("child 1") >>> child2 = TreeNode("child 2") >>> root.children = [child1, child2] >>> root.children [TreeNode("child 1"), TreeNode("child 2")] >>> del root.children >>> root.children []
在這里,您首先創建一個root節點來開始填充樹。然后創建兩個新節點并.children使用列表將它們分配給它們。該del語句觸發內部刪除器方法.children并清除列表。
創建向后兼容的類 API
如您所知,屬性將方法調用轉換為直接屬性查找。此功能允許您為您的類創建干凈的 Pythonic API。您可以公開公開您的屬性,而無需 getter 和 setter 方法。
如果您需要修改計算給定公共屬性的方式,則可以將其轉換為屬性。屬性可以執行額外的處理,例如數據驗證,而無需修改您的公共 API。
假設您正在創建一個會計應用程序并且您需要一個基類來管理貨幣。為此,您創建了一個Currency公開兩個屬性的類,.units并且.cents:
class Currency: def __init__(self, units, cents): self.units = units self.cents = cents # Currency implementation...
這個類看起來干凈和 Pythonic。現在假設您的要求發生了變化,您決定存儲美分的總數而不是單位和美分。從您的公共 API 中刪除.units和.cents使用類似的東西.total_cents會破壞多個客戶端的代碼。
在這種情況下,property()保持當前 API 不變是一個很好的選擇。以下是您可以如何解決該問題并避免破壞客戶代碼的方法:
# currency.py CENTS_PER_UNIT = 100 class Currency: def __init__(self, units, cents): self._total_cents = units * CENTS_PER_UNIT + cents @property def units(self): return self._total_cents // CENTS_PER_UNIT @units.setter def units(self, value): self._total_cents = self.cents + value * CENTS_PER_UNIT @property def cents(self): return self._total_cents % CENTS_PER_UNIT @cents.setter def cents(self, value): self._total_cents = self.units * CENTS_PER_UNIT + value # Currency implementation...
現在,您的類存儲美分的總數,而不是獨立的單位和美分。但是,您的用戶仍然可以在他們的代碼中訪問和更改.units和.cents并獲得與以前相同的結果。來試試看吧!
當您編寫許多人要在其上構建的內容時,您需要保證對內部實現的修改不會影響最終用戶如何使用您的類。
覆蓋子類中的屬性
當您創建包含屬性的 Python 類并在包或庫中發布它們時,您應該期望您的用戶使用它們做很多不同的事情。其中之一可能是對它們進行子類化以自定義其功能。在這些情況下,您的用戶必須小心并注意一個微妙的問題。如果您部分覆蓋了一個屬性,那么您將失去未被覆蓋的功能。
例如,假設您正在編寫一個Employee類來管理公司內部會計系統中的員工信息。您已經有一個名為 的類Person,并且您考慮將其子類化以重用其功能。
Person具有作為.name屬性實現的屬性。的當前實現.name不滿足以大寫字母返回名稱的要求。以下是您最終解決此問題的方法:
# persons.py class Person: def __init__(self, name): self._name = name @property def name(self): return self._name @name.setter def name(self, value): self._name = value # Person implementation... class Employee(Person): @property def name(self): return super().name.upper() # Employee implementation...
在 中Employee,您重寫.name以確保在訪問屬性時,您獲得大寫的員工姓名:
>>>
>>> from persons import Employee, Person >>> person = Person("John") >>> person.name 'John' >>> person.name = "John Doe" >>> person.name 'John Doe' >>> employee = Employee("John") >>> employee.name 'JOHN'
偉大的!Employee根據您的需要工作!它使用大寫字母返回名稱。然而,隨后的測試發現了一個意想不到的行為:
>>>
>>> employee.name = "John Doe" Traceback (most recent call last): ... AttributeError: can't set attribute
發生了什么?好吧,當您覆蓋父類中的現有屬性時,您將覆蓋該屬性的全部功能。在此示例中,您僅重新實現了 getter 方法。因此,.name失去了基類的其余功能。您不再有 setter 方法了。
這個想法是,如果您需要覆蓋子類中的屬性,那么您應該在手頭的新版本的屬性中提供您需要的所有功能。
結論
一個屬性是一種特殊類型的類成員,它提供的功能是在常規屬性和方法之間的某處。屬性允許您在不更改類的公共 API 的情況下修改實例屬性的實現。能夠保持 API 不變有助于避免破壞用戶在舊版本類之上編寫的代碼。
屬性是在類中創建托管屬性的Pythonic方式。它們在現實世界的編程中有幾個用例,使它們成為您作為 Python 開發人員技能集的一個很好的補充。
在本教程中,您學習了如何:
使用 Python創建托管屬性property()
執行惰性屬性評估并提供計算屬性
避免帶有屬性的setter和getter方法
創建read-only、read-write和write-only屬性
為您的類創建一致且向后兼容的 API
您還編寫了幾個實際示例,引導您了解property().?這些示例包括輸入數據驗證、計算屬性、記錄代碼等。
Python
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。