作者ddavid (謊言接線生)
看板Python
標題Re: [問題] @property 真正的運用是啥
時間Wed Jan 15 12:03:40 2025
※ 引述《littrabble (littrabble)》之銘言:
: @property
: def name(self):
: return self._name
: @name.setter
: def name(self, new_name):
: self._name = new_name
: 然後可以使用 instance p,
: p.name 取值, p.name = 1 設值
: 我的疑問是,
: 1. 這根本無法保護變數,為什麼教程還要說這種寫法保護變數
: 2. 加那個@property @name.setter, 到底有什麼好處?
: 我如果不使用@property, 而是把方法名稱改成 get_name, 跟 set_name 程式碼讀起來,不是更清楚明白嗎?
: 有沒有很有經驗的大大,能幫我解惑一下
: 感恩
我們從幾個角度來思考這個問題:
1. 語感
當我們使用 class Human 時,在普遍的語感上,屬性或成員是一些這個 class
實例具有的狀態或資訊:
human1 = Human(...)
print(human1.name) # 印出 human1 的 名字
而方法在語感上是一些行為或動作:
human1.dance() # 讓 human1 進行 跳舞 這個行為
那我們來思考一下,如果我們採用 get_name,那印出姓名會是這樣的語感:
print(human1.get_name()) # 讓 human1 進行 取得自己姓名 這個行為,然後印出
# 這個行為的結果
比較一下兩種印出名字的語感,是不是採用屬性或成員比較自然、不拐彎抹角?
然而,這也不代表 get/set method 形式就要完全捨棄。在 PEP 8 中有提到相
關的建議。
首先,如果是超級單純的直接成員存取,也沒有特殊的限制邏輯考量,則你應該
乾脆地直接使用公開成員,什麼 @property 或 get/set method 都免了。
再來,如果這個邏輯變得複雜,我們隨時都可以使用 @property 進行包裝,讓
使用方式跟公開成員完全相同,但內部處理邏輯改變。
但是,使用 @property 的情況下,因為其語感給使用者就像是直接存取一個成
員變數,所以我們會希望就算它有包裝一些處理邏輯,但這些處理邏輯不要帶來副作
用,也不要是太過昂貴的操作,因為使用者不會設想一個簡單的:
human1.name = "ddavid"
操作背後居然會導致他的銀行帳戶變成我的,或者要執行三天只因為真的去跑戶
政事務所改名流程。當你真的想要讓上面兩件事情發生,使用 method 來表現的語感
就更為合適:
human1.set_bank_account_name("ddavid")
human1.set_id_card_name("ddavid")
法律小提示:銀行帳戶沒法轉讓啦,所以放心吧。直接轉帳給我就好啦(誤)
以下是 PEP 8 相關原文:
For simple public data attributes, it is best to expose just the attribute
name, without complicated accessor/mutator methods. Keep in mind that Python
provides an easy path to future enhancement, should you find that a simple
data attribute needs to grow functional behavior. In that case, use
properties to hide functional implementation behind simple data attribute
access syntax.
Note 1: Try to keep the functional behavior side-effect free, although
side-effects such as caching are generally fine.
Note 2: Avoid using properties for computationally expensive operations; the
attribute notation makes the caller believe that access is (relatively) cheap.
2. 保護變數
原 po 可能誤解的一點是,@property 的保護變數是跟直接暴露成員相比的。在
保護變數這一點上,它跟 set/get method 效果相差不大。
比如相較於:
class Human:
def __init__(height: float):
self.height = height
human1 = Human(170.1)
human1.height = -1 # 亂給身高為負值
使用以下方法可以對此做出保護:
class Human:
def __init__(self, height: float):
self._height = height
@property
def height(self):
return self._height
@height.setter
def height(self, value: float):
if value < 0:
raise ValueError("Height cannot be negative")
self._height = value
當然你一樣可以用 set_height 的寫法做到這一點:
def set_height(self, value: float):
if value < 0:
raise ValueError("Height cannot be negative")
self._height = value
但當考量到前述的語感理由,在 height 是個單純屬性處理的情況下,就沒什麼
必要強調操作性。
同時,我們也可以拿掉 setter/getter 其中之一,讓其變成可讀不可寫或可寫
不可讀,這也是一種保護。
當然我們知道,即便使用 _ 甚至 __ 前綴的成員,在 Python 中始終有手段直
接操作原始成員,因為 Python 把這些判斷留給 programmer。
3. 封裝邏輯
比如說,對於人類而言,BMI 語感上作為一個很單純的屬性值也很直覺。可是當
我們已經存了身高體重,額外存一個 BMI 好像在某些情況下有點多餘。於是我們就
可以在維持其屬性語感的前提下把邏輯包裝起來:
class Human:
def __init__(self, height: float, weight: float):
self.height = height
self.weight = weight
@property
def bmi(self):
return self.weight / (self.height * self.height)
所以這麼做後,我們就可以用 human1.bmi 這樣直覺的方式取得這個人的 BMI,
而且在身高體重有變化時還可以自然跟著變化。而因為這不是很昂貴的運算,所以每
次取都算一下也沒太大關係。
同時,因為我們沒有給予 setter,也表達出了對於這個值的保護是唯讀的,我
們不能手改 BMI 或想藉由改 BMI 去影響身高體重值之類。
如前述,如果語感上要強調 BMI 每次都是計算出來的,我認為寫成 get_bmi 的
方法也無不可。
--
「可是妳......不是天使嗎?」
「天使?」她緩緩的轉過頭來,用悲傷的表情。「天使,只不過是神創造出來的
不死玩偶。」
「而神,也只不過是詛咒下的偽善使者。」
--星.幻.夢的傳說
--
※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 125.229.62.213 (臺灣)
※ 文章網址: https://www.ptt.cc/bbs/Python/M.1736913822.A.4BF.html
※ 編輯: ddavid (125.229.62.213 臺灣), 01/15/2025 14:32:22
推 sating00: 就…如果你寫的code也不是什麼大型專案,沒必要這樣設 01/15 20:23
→ sating00: 計這些保護,除非設計理念跟有哲學上的潔癖(像我) 01/15 20:23
這倒不完全跟專案大小有關,跟系統是否接觸外部或者有其他協作者比較有關。
推 melancholy07: 討論推 01/15 21:01
※ 編輯: ddavid (125.229.62.213 臺灣), 01/16/2025 10:46:49
→ leolarrel: 我跟一樓一樣,寫程式有潔癖 01/16 11:41
推 cuteSquirrel: 獅子習慣良好 01/17 07:33
→ mantour: 但這樣的寫法使用者可能會以為自己在單純的賦值和取值, 01/18 17:29
→ mantour: 等到出錯才會意識到這不是一個單純的屬性,用getter/set 01/18 17:29
→ mantour: ter明示不是比較不容易誤解嗎。 01/18 17:29
→ Hsins: 在使用物件時,沒有經過 @property 裝飾器修飾的,無法這樣 01/18 18:07
→ Hsins: 操作;為了實現物件導向程式開發的封裝概念,本來也不應該 01/18 18:07
→ Hsins: 在 class 以外直接操作屬性。所以如果產生「我不知道是在直 01/18 18:07
→ Hsins: 接操作屬性還是使用 getter 或 setter 耶」這樣的想法,需 01/18 18:07
→ Hsins: 要回來想想看審視一下當下的寫法有沒有問題。 01/18 18:07
→ Hsins: 我上面說的可能有些繞口,簡單來說就是在 OOP 的理念中,屬 01/18 18:09
→ Hsins: 性本來就不該被直接操作。 01/18 18:09
推 ck574b027: 我試著比樓上更精確些。有封裝的概念時, 01/18 23:30
→ ck574b027: 我沒給的,外人本來就不能要;外人能拿到的代表有控制 01/18 23:32
所以 PEP 8 才會強調不應該帶有副作用,基本概念就是保持使用者把它當直接
的屬性操作不會誤解出事。比如說作者只是加了一堆型別檢查、型別轉換、範圍檢查
等等,都通過了以後最後還是簡單的直接賦值,那就不用擔心使用者理解錯誤。
事實上大概就是 Pydantic 之類做的事情。
又或是你內部存公尺,但允許使用者用英呎存取,這只是很簡單的轉換。
還有像 requests 裡面呼叫 API 回應的 Response.ok:
@property
def ok(self):
"""Returns True if :attr:`status_code` is less than 400, False if not.
This attribute checks if the status code of the response is between
400 and 600 to see if there was a client error or a server error. If
the status code is between 200 and 400, this will return True. This
is **not** a check to see if the response code is ``200 OK``.
"""
try:
self.raise_for_status()
except HTTPError:
return False
return True
它確實不是單純的取值,但概念很單純,雖然 raise_for_status() 裡面其實多
做了一些事情,但沒有會影響狀態的東西,所以也沒太大問題。
※ 編輯: ddavid (114.44.6.70 臺灣), 01/28/2025 06:39:06
→ w0005151: 純個人經驗來說,不是在開發lib或框架,真的沒必要用 01/31 00:10
→ w0005151: 大部分時候遵守KISS原則帶來的好處勝於一切 01/31 00:11