iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Software Development

Python十翼:與未來的自己對話系列 第 9

[Day09] 三翼 - property:實例說明

  • 分享至 

  • xImage
  •  

今日我們透過解題,來練習看看如何活用property。以下會以prop來稱呼由property建立的property instance

問題

  1. 建立一個Color class
    • 接受一個參數color,其為一代表(r, g, b)格式的tuple
    • 實作一個名為colorprop ,具有gettersetter功能。
    • 實作一個名為hexprop,可動態計算prop colorHex color code,並具備快取機制。
  2. 建立一個class Red繼承Color
    • 實作一個名為colorprop ,其只有getter功能,而沒有setter功能。

解法1

首先定義Color class,並實作__init__

  • self.color = color暗示其會呼叫color propsetter,來幫忙設定color
  • self._hexhex prop底下真正包含的值,先於此定義。
# 01
class Color:
    def __init__(self, color):
        self.color = color
        self._hex = None

接著實作color propgettersetter

  • getter會返回self._color這個color prop底下真正包含的值。
  • setter會先呼叫_validate做兩個檢查,確保給定的color值為tuple型態且tuple中每個元素都是範圍在0~255之間的int。如果通過的話,則將給定的color指定給self._color,並將self._hex設為Noneself._hex = None相當於每次呼叫colorsetter時會清除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 propsetter來直接指定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()將呼叫delegateColor,所以應該寫為:

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中已經overwritecolor prop,其只有具備getter功能。而在Red__init__將呼叫delegateColor__init__時,其中的self.color = color會需要呼叫color propsetter

請注意這邊有一個在使用property時常見的誤解。有些朋友可能會覺得我們於Red中只overwritecolorgetter,但是沒有overwritecolorsetter,所以不應該報錯呀?

這個誤解的來源是將gettersetter分別視為了兩個prop。但是正確的思路是,Color class其內的color prop同時實作有fgetfset,而Red class其內的color prop只有實作fget。而當我們由red來呼叫color時,self相當於red,而self.color = color相當於red.color = color,由於該prop沒有實作fset,所以會報錯。

我們將於解法2解決這個問題。

解法2

我們將原先color propsetter的邏輯,移到新的_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來幫助我們枚舉各種顏色,假設我們現在需要建立RedGreenBlue三個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

解法3

由於解法2裡我們於繼承Colorclass內都要實作color prop,我們開始思考是不是能把這個prop抽象出來。

我們嘗試建立一個ReadColorOnly class來包住這個prop。這麼一來後續的class繼承ReadColorOnlyColor後,只需要實作__init__即可。

如果只需要建立RedGreenBlue三個class時,我們會傾向解法2。但如果後續有很多像LuckyColorclass需要建立時,或許解法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

Code

本日程式碼傳送門


上一篇
[Day08] 三翼 - property:核心原理與基本型態
下一篇
[Day10] 四翼 - Descriptor:Non-Data Descriptor vs Data Descriptor
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言