看板 Python 關於我們 聯絡資訊
※ 引述《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