今日我們透過解題,來練習看看如何活用property。以下會以prop來稱呼由property建立的property instance。
Color class:
color,其為一代表(r, g, b)格式的tuple。color的prop ,具有getter及setter功能。hex的prop,可動態計算prop color的Hex color code,並具備快取機制。class Red繼承Color:
color的prop ,其只有getter功能,而沒有setter功能。首先定義Color class,並實作__init__。
self.color = color暗示其會呼叫color prop的setter,來幫忙設定color。self._hex為hex prop底下真正包含的值,先於此定義。# 01
class Color:
def __init__(self, color):
self.color = color
self._hex = None
接著實作color prop的getter及setter。
getter會返回self._color這個color prop底下真正包含的值。setter會先呼叫_validate做兩個檢查,確保給定的color值為tuple型態且tuple中每個元素都是範圍在0~255之間的int。如果通過的話,則將給定的color指定給self._color,並將self._hex設為None。self._hex = None相當於每次呼叫color的setter時會清除hex prop的快取機制。# 01
class Color:
...
def _validate(self, color):
if not isinstance(color, tuple):
raise ValueError('color value must be a tuple')
if not all(isinstance(c, int) and 0 <= c <= 255 for c in color):
raise ValueError(
'color value must be an integer and 0 <= color value <=255')
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._validate(color)
self._color = color
self._hex = None # purge cache
再來實作hex prop。先確認self._hex是否為None,若是的話代表第一次呼叫或是快取已被清除,此時需真正計算Hex color code,最後再返回self._hex。請注意我們使用self.color來存取self._color,而非直接使用self._color。雖然兩種方式都可以,但是當有property這種公開的interface時,建議優先使用。因為這麼一來當interface有問題時,身為第一個使用者的我們很容易發現。
# 01
class Color:
...
@property
def hex(self):
if self._hex is None:
self._hex = ''.join(f'{c:02x}' for c in self.color)
return self._hex
最後實作Red class。因為我們很明確知道紅色的color為(255, 0, 0),所以可以直接在__init__中利用super()來請Color class來幫忙設定。
# 01
class Red(Color):
def __init__(self):
super().__init__((255, 0, 0))
但是這麼寫有個問題是,我們可以使用color prop的setter來直接指定color,像是下面這樣就變成「紅皮綠底」了啊。
>>> red = Red()
>>> red.color = (0, 255, 0) # green
一個解決的方法是,於Red中實作自己的color prop。
class Red(Color):
...
@property
def color(self):
return self.color
可是這麼寫也不行呀,self.color會不斷再呼叫self.color直到報錯。此時我們真正需要的是當呼叫self.color時,利用super()將呼叫delegate回Color,所以應該寫為:
class Red(Color):
...
@property
def color(self):
return super().color
至此我們完成Red class的實作。但當試圖建立red時,卻發現會raise AttributeError,這是怎麼一回事呢?
>>> red = Red() # AttributeError: property 'color' of 'Red' object has no setter
原來是因為我們在Red中已經overwrite了color prop,其只有具備getter功能。而在Red的__init__將呼叫delegate回Color的__init__時,其中的self.color = color會需要呼叫color prop的setter。
請注意這邊有一個在使用property時常見的誤解。有些朋友可能會覺得我們於Red中只overwrite了color的getter,但是沒有overwrite的color的setter,所以不應該報錯呀?
這個誤解的來源是將getter與setter分別視為了兩個prop。但是正確的思路是,Color class其內的color prop同時實作有fget及fset,而Red class其內的color prop只有實作fget。而當我們由red來呼叫color時,self相當於red,而self.color = color相當於red.color = color,由於該prop沒有實作fset,所以會報錯。
我們將於解法2解決這個問題。
我們將原先color prop中setter的邏輯,移到新的_set_color function 中。並將Color.__init__中的self.color = color改為self._set_color(color)。這麼一來就能符合題意,巧妙的解決問題。
# 02
class Color:
def __init__(self, color):
self._set_color(color)
self._hex = None
def _validate(self, color):
if not isinstance(color, tuple):
raise ValueError('color value must be a tuple')
if not all(isinstance(c, int) and 0 <= c <= 255 for c in color):
raise ValueError(
'color value must be an integer and 0 <= color value <=255')
def _set_color(self, color):
self._validate(color)
self._color = color
self._hex = None # purge cache
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._set_color(color)
@property
def hex(self):
if self._hex is None:
self._hex = ''.join(f'{c:02x}' for c in self.color)
return self._hex
此外,可以善用Enum來幫助我們枚舉各種顏色,假設我們現在需要建立Red、Green及Blue三個class時,可以這麼寫:
# 02
from enum import Enum
...
class MyColor(Enum):
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Red(Color):
def __init__(self):
super().__init__(MyColor.RED.value)
@property
def color(self):
return super().color
class Green(Color):
def __init__(self):
super().__init__(MyColor.GREEN.value)
@property
def color(self):
return super().color
class Blue(Color):
def __init__(self):
super().__init__(MyColor.BLUE.value)
@property
def color(self):
return super().color
由於解法2裡我們於繼承Color的class內都要實作color prop,我們開始思考是不是能把這個prop抽象出來。
我們嘗試建立一個ReadColorOnly class來包住這個prop。這麼一來後續的class繼承ReadColorOnly及Color後,只需要實作__init__即可。
如果只需要建立Red、Green及Blue三個class時,我們會傾向解法2。但如果後續有很多像LuckyColor的class需要建立時,或許解法3會是比較好的選擇。
# 03
import random
...
class ReadColorOnly:
@property
def color(self):
return super().color
class Red(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.RED.value)
class Green(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.GREEN.value)
class Blue(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.BLUE.value)
class LuckyColor(ReadColorOnly, Color):
def __init__(self):
super().__init__(tuple(random.choices(range(256), k=3)))
本日內容啟發自python-deepdive-Part 4-Section 06-Single Inheritance-Delegating to Parent。