筆者以前在學習 Java 的時候,學過用 封裝(Encapsulation)來保護變數資料,並透過 Getter 與 Setter 方法操作資料來讓程式碼更穩固,Getter 方法寫出回傳資料的內容與方式,Setter 方法則是寫下設定或更新資料的詳細邏輯。有些變數的資料在程式裡有特殊功能或者特別重要,因此需要 Getter 與 Setter 來保護它們不被隨意取用或更改。
在 Python 要實現這種(接近於)封裝,有一種特殊且方便的工具:Property(屬性),可以用簡潔的語法寫出 Getter 與 Setter 的功能。這則筆記將簡介 Python 的 Property 語法,並且用實例程式碼介紹使用 Getter 與 Setter 有什麼好處、以及使用時該注意的地方。相信你閱讀之後,將會再提升 Python 物件導向程式設計的技能!
目錄
Property 簡介
Python 的 Property(屬性)是一種 Decorator(裝飾器),也就是 Property 的語法要被寫在函式定義 def
的前面,Property 的裝飾器主要有 3 種語法:
- Getter:
@property
- Setter:
@變數名稱.setter
- Deleter:
@變數名稱.deleter
以下將用範例程式碼簡介這 3 種語法,首先,我們先複習一下最基本的類別(class)以及物件內的資料操作:
class Person:
def __init__(self, name):
self.name = name
這個類別的設計十分簡單,你在初始化資料後,可以隨意取用、更新、甚至刪除物件內部的變數資料:
>>> p = Person("Wilson Huang")
>>> p.name
'Wilson Huang'
>>> p.name = "Michael Jackson"
>>> p.name
'Michael Jackson'
>>> del p.name
>>> p.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'name'
接下來,我們簡單加上一行 @property
的裝飾器,Property 立刻就讓資料變成只能取用,不能更新或者刪除:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
(補充:前單底線的變數(_name
)是用來提示此變數「最好不要更動」的慣例,詳情請參考好豪的 Python 底線用法教學文章)
>>> p = Person("Wilson Huang")
>>> p.name
'Wilson Huang'
>>> p.name = "Michael Jackson"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> del p.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
上方範例的 @property
讓函數名稱(name
)變成了 Getter 方法,上方範例中不能更新或刪除資料是因為我們沒寫清楚該怎麼做,接下來我們就用 @變數名稱.setter
與 @變數名稱.deleter
把如何更新與刪除資料的邏輯寫出來:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, new_name):
self._name = new_name
@name.deleter
def name(self):
del self._name
>>> p = Person("Wilson Huang")
>>> p.name
'Wilson Huang'
>>> p.name = "Michael Jackson"
>>> p.name
'Michael Jackson'
>>> del p.name
>>> p.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in name
AttributeError: 'Person' object has no attribute '_name'
加上 Setter 與 Deleter 後,再次可以達成更新與刪除資料了。你是否發現,現在 Property 做到的事情跟最一開始只有 3 行程式碼的簡單類別範例一樣呢?差別在於,現在我們透過 Property 語法把類別該如何取用、更新、還有刪除資料明確寫出來,接下來的篇幅,筆者好豪將介紹這麼做的好處。
Property 的好處
保護變數不可被更動
當我們需要保護類別內的屬性資料、不希望被使用者隨意更動時,Property 可以為我們達成這項保護的目標。
同樣先從簡單的類別舉例,假設類別內資料要記錄一個人的出生年份與年齡,一個 1994 年出生的人,今年(2023 年)該被設定成 29 歲:
class Person:
def __init__(self, birth_year):
self.birth_year = birth_year
self.age = age
然而,變數沒有被保護的情況下,1994 年出生的人,還是可以把年齡變數設定成只有 3 歲喔!邏輯不通、但是程式語法可行:
>>> p = Person(1994, 29)
>>> p.birth_year
1994
>>> p.age
29
>>> p.age = 3
>>> p.birth_year
1994
>>> p.age
3
為了不讓這種邏輯不通的事情發生,我們可以用 @property
來保護變數,讓特定資料只能被取用、不可被更改:
import datetime
class Person:
def __init__(self, birth_year):
self.birth_year = birth_year
@property
def age(self):
now = datetime.datetime.now()
return now.year - self.birth_year
@property
改寫後,年齡變數被 Property 保護、不可更動,但是出生年份資訊是可以更動的,而且可以發現更新出生年份資料後、年齡也會隨之被計算成正確的資料:
>>> p = Person(1994)
>>> p.birth_year
1994
>>> p.age
29
>>> p.age = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> p.birth_year = 1983
>>> p.age
40
用 @property
之後,類別內的資料是不是變得更安全可靠了呢!
(如果你的類別內所有屬性資料都需要被保護不被更動,推薦你改成使用 dataclass
內的 frozen
功能,語法會更加簡潔!)
更高彈性的 Get 與 Set 邏輯
如果類別內的狀態會不斷更新,你的資料取用與更新就必需要與內部狀態聯動。例如我們設計一個簡單的類別記錄每個人的姓氏、名字、加上全名:
class Person:
def __init__(self, first_name, last_name, full_name):
self.first_name = first_name
self.last_name = last_name
self.full_name = full_name
在這個類別的資料是可以任意更動的,如果只改了名字、但是忘記手動把全名一起改掉的話,就會發生資料與內部狀態不一致的情形:
>>> p = Person("Wilson", "Huang", "Wilson Huang")
>>> p.first_name
'Wilson'
>>> p.last_name
'Huang'
>>> p.full_name
'Wilson Huang'
>>> p.first_name = "Kenny"
>>> p.first_name
'Kenny'
>>> p.last_name
'Huang'
>>> p.full_name
'Wilson Huang'
用 @property
的 Getter 寫法就能解決問題,讓 Getter 取決於類別內部的目前狀態來回傳資料:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return self.first_name + " " + self.last_name
>>> p = Person("Wilson", "Huang")
>>> p.full_name
'Wilson Huang'
>>> p.first_name
'Wilson'
>>> p.last_name
'Huang'
>>> p.full_name
'Wilson Huang'
>>> p.first_name = "Kenny"
>>> p.first_name
'Kenny'
>>> p.last_name
'Huang'
>>> p.full_name
'Kenny Huang'
# 注意,目前沒有設定 Setter,所以 full_name 不可更動
>>> p.full_name = "Michael Jackson"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Property 也讓 Setter 設定資料的時候有更高的彈性。例如,可以在設定全名之後,同時讓類別內的姓氏跟名字資料都更新;並且做點文字處理、幫使用者刪除掉多餘的空白,避免打錯字:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return self.first_name + " " + self.last_name
@full_name.setter
def full_name(self, new_name):
self.first_name, self.last_name = \
[x for x in new_name.split(" ") if x != '']
>>> p = Person("Wilson", "Huang")
>>> p.full_name
'Wilson Huang'
>>> p.first_name
'Wilson'
>>> p.last_name
'Huang'
# 多打了空白也沒關係!
>>> p.full_name = " Michael Jackson "
>>> p.full_name
'Michael Jackson'
>>> p.first_name
'Michael'
>>> p.last_name
'Jackson'
資料驗證
筆者認為用 Getter 或 Setter 最重要的好處之一就是驗證資料,如果資料有任何不合理狀況,我們可以為使用者建立基本的防呆機制。
舉例而言,人的年齡超過 200 歲或是小於 0 歲都是不合理的,要是使用者手滑、不小心輸入詭異的年齡數字,可以透過 @變數名稱.setter
驗證資料、擋下不恰當的數字、避免建立無效資料:
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, new_age):
if new_age < 0 or new_age > 200:
raise ValueError("輸入為不合理的年齡數字!")
self._age = new_age
>>> p = Person(18)
>>> p.age
18
>>> p.age = 30
>>> p.age
30
>>> p.age = -23
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in age
ValueError: 輸入為不合理的年齡數字!
>>> p.age = 9527
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in age
ValueError: 輸入為不合理的年齡數字!
使用警告
Property 的語法跟撰寫邏輯都很簡單,本文也介紹了不少它的好處,但是還是有些眉眉角角是使用 Property 需要注意的。
首先,雖然前面提到 Property 有保護變數的好處,但是 Python 實際上沒有 100% 做到封裝(Encapsulation)、或者說變數並不完全是私有(Private)的。換言之,只要使用者有心為之,還是可以繞道更動被 Property 保護的變數資料。例如:
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
# 一般情況下,Property 裝飾過的變數是不能更動的
>>> p = Person(18)
>>> p.age
18
>>> p._age
18
>>> p.age = 30
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
# 但使用者其實可以「繞過」Property、直接更改類別內的屬性資料
>>> p._age = 30
>>> p.age
30
>>> p._age
30
關於這點,Python 社群有「請當遵守規定的成熟大人(”We are all consenting adults”)」這句共同默契,意思是,Python 當然還是有辦法做到完整保護變數不被更動,但是比起花心力處理這種問題,不如大家都說好遵守這些寫程式的慣例、會比較輕鬆且有效率。也就是,有前底線(_var
)或 Property 裝飾過的變數,請大家守規矩、乖乖不要亂更動!
另外,筆者好豪也在工作中從前輩的經驗中學到,計算成本高、或者資料取得邏輯複雜時,並不適合用 Property 寫成 Getter,因為 Getter 原本的目的單純是取得資料、不該有意外狀況。從資料庫讀取資料就是典型的不適合寫成 Getter 的範例:
# Pseudo-Code
@property
def data(self):
return database.query("SELECT * FROM my_database")
一來,從資料庫讀取資料可能計算量大、一次要花很久的時間;再者,讀取資料庫有可能發生錯誤,Getter 如果要處理這每一種錯誤,會變得太過繁雜。
總之,低計算量、而且不會有意外(Exception)的取用資料行為,才適合用 Property 寫成 Getter。
結語
這篇文章介紹 Property 可以保護變數不被更動、提供更高彈性的取用與更動資料邏輯、還有資料驗證等等好處。然而實際寫 Python 程式的時候,我們通常還是該從簡單的類別還有單純的屬性來儲存資料就好、避免程式碼從一開始就不必要地複雜,等到我們確定需要上述的幾種 Property 的好處時,再用 @property
來改寫就好。
參考資料:
- Udemy 線上課程: Python Deep Dive – OOP
- 《Python 神乎其技》
- PEP 549 – Instance Descriptors
- YouTube: property decorator – deep dive
你正在努力自學 Python 嗎?筆者整理了 Python 實用自學資源,推薦熱愛自學的你繼續閱讀!如果這篇文章有幫助到你,也歡迎追蹤好豪的 Facebook 粉絲專頁,我會持續分享 Python 相關知識;也歡迎點選下方按鈕將本文加入書籤、或者分享給更多正在學 Python 的朋友。