Enumerations 列舉

什麼是列舉?

定義

The properties of an enumeration are useful for defining an immutable, related set of constant values that may or may not have a semantic meaning. (https://peps.python.org/pep-0435/#proposed-semantics-for-the-new-enumeration-type)

列舉:具有關連性的一組常數成員。

範例

最常被用來舉例的,就是 Color RGB。

PEP 435 中另外還舉了這些例子:

  • 星期幾:Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday

  • 學校成績分級(美):A, B, C, D, F

  • 錯誤值和狀態的定義(error status values and states within a defined process)

中英譯詞

英文 繁體譯詞
Enumerations 列舉
members 成員
alias 別名
lookup 查找
containment 內含
iterate/iterating 迭代
extend 延伸
customize 自定;定制

參考資料:

本節老師的原始碼


為什麼?

為什麼我們要使用 Enumerations(列舉)呢?

  • 列舉的目的是什麼?

  • 為什麼要使用列舉?有什麼獨特的價值讓它單獨存在?

  • 什麼時候應該使用,什麼時候應該避免?


特性 & 語法

特性 說明 & 語法
可雜湊的(hashable) 可以像 dictionary 的 key 來取用成員
或 Set 中的 elements
可迭代的(iterable) for color in Color:
不可變的(immuable) 無法修改、新增、刪除
繼承(inherits)限制 僅在無成員時(例:pass),才能被繼承
以名稱(name)取用成員 Color.RED, Color['RED']
(註:Enum 實作了 __getitem__ 方法)
以值(value)取用成員 Color(1)
成員是否在列舉中 in :arrow_right: if Color.RED in Color:
比較是否同一成員 is :arrow_right: if Color.RED is Color.BLUE:
比較成員值是否相等 == :arrow_right: if Color.RED == 1:

參考資料:


alias 別名

兩個列舉成員的名稱不能相同,但是可以有相同的值。

假設成員 A 和 B 的值相同,先定義的成員會成為預設值(本例為 A),此時 B 就是別名。

按值尋找 A 和 B 的值返回的是 A。按名稱尋找 B,返回的也是 A。

from enum import Enum

class Color(Enum):
	red = 1
	crimson = 1
	carmine = 1
	blue = 2
	aquamarine = 2

Color.red is Color.crimson
# 輸出:True

特性

指令 成員 member 別名 alias
查找 Lookups with aliases
內含 Containment
迭代 Iterating Aliases

查找 Lookups with aliases

查找(lookup)相關指令,回傳值一律只有成員(member)。

# 程式碼續前段

Color(1)
# 輸出:<Color.red: 1>

Color['crimson']
# 輸出:<Color.red: 1>

內含 Containment

# 程式碼續前段

Color.crimson in Color
# 輸出:True

迭代 Iterating Aliases

# 程式碼續前段

list(Color)
# 輸出:[<Color.red: 1>, <Color.blue: 2>]

members + aliases

那麼要如何得知 enum 中所有的元素,包含成員和別名呢?

__members__

# 程式碼續前段
Color.__members__

# 以下為輸出:
mappingproxy({'red': <Color.red: 1>,
              'crimson': <Color.red: 1>,
              'carmine': <Color.red: 1>,
              'blue': <Color.blue: 2>,
              'aquamarine': <Color.blue: 2>})

我們前面不是提到,只有成員會在迭代中出現嗎?

如果使用 __members__,成員和別名,都可以在迭代中出現。

# 程式碼續前段
list(Color)
# 輸出:[<Color.red: 1>, <Color.blue: 2>]

list(Color.__members__.items())
# 以下為輸出:
[('red', <Color.red: 1>),
 ('crimson', <Color.red: 1>),
 ('carmine', <Color.red: 1>),
 ('blue', <Color.blue: 2>),
 ('aquamarine', <Color.blue: 2>)]

利用迭代,我們可以篩選出所有別名:

# 程式碼續前段
[name for name, member in Color.__members__.items() if member.name != name]

# 輸出:['crimson', 'carmine', 'aquamarine']

實際應用

假設你要整合不同系統,不同系統回傳的資料值雖然不同,其實只是命名不同,代表的意思卻相同。

這時就可以將你自己系統原本的命名設為成員,要整合的系統,其命名設為別名。

Our System System 1 System 2 Meaning
IN_PROGRESS REQUESTING PENDING The request is in progress
SUCCESS OK FULFILLED The request was completed successfully
ERROR NOT_OK REJECTED The request was failed

想法:可以應用在應用程式的不同語言版本嗎?

我覺得可以,但有更好的做法。

@enum.unique 裝飾器

如果你想確認及避免,在 enum 中出現別名 alias,方法很簡單:@enum.unique

from enum import Enum

@enum.unique
class Status(Enum):
    ready = 1
    done_ok = 2
    errors = 3

try:
    @enum.unique
    class Status(Enum):
        ready = 1
        waiting = 1
        done_ok = 2
        errors = 3
except ValueError as ex:
    print(ex)

參考資料:


Customizing and Extending Enumerations

定制列舉 & 延伸列舉

定制列舉

燒腦版說明:Members of the enumerations are instances of the enumeration class, so we can implement methods in that class, and each member will have that method (boud to itself) available.

免動腦說明: Implement dunder methods to customize the behavior of Python enum classes.

關於 instance 綁定(Binding),可參考 Section 2: Classes (17-22)

看實例應該比較容易了解:

from enum import Enum

class Color(Enum):
    red = 1
    green = 2
    blue = 3
    
    def purecolor(self, value):
        return {self: value}

Color.red.purecolor(100), Color.blue.purecolor(200)
# 輸出:({<Color.red: 1>: 100}, {<Color.blue: 3>: 200})

Dunder Methods

我們可以透過上述特性,實作以下 Dunder Methods 來達到定制列舉的目的(by ChatGPT)。

Dunder Method 說明
__members__ 控制列舉成員的順序。
__repr__ 自訂列舉實例的表示方式,通常是用於顯示列舉成員的名稱。
__str__ 自訂列舉實例的字串表示方式,通常是用於顯示列舉成員的可讀性較高的名稱。
__format__ 控制使用 format() 函式格式化列舉實例時的輸出。
__eq__ 自訂列舉實例的相等(equal)比較行為。
__ne__ 自訂列舉實例的不相等(not equal)比較行為。
__lt__ 自訂列舉實例的小於(less than)比較行為。
__le__ 自訂列舉實例的小於等於(less than or equal to)比較行為。
__gt__ 自訂列舉實例的大於(greater than)比較行為。
__ge__ 自訂列舉實例的大於等於(greater than or equal to)比較行為。
__bool__ 定義列舉實例的布林值行為,用於判斷列舉成員是否為真值(true)。
__hash__ 定義列舉實例的雜湊(哈希)值,通常用於支援集合(set)操作。
__call__ 讓列舉實例可以像函式一樣被呼叫。
__getitem__ 讓列舉成員可以透過索引或切片操作獲取。or…
可以通過索引或成員名稱獲取列舉的成員。
__iter__ 定義列舉實例的迭代行為,使其可以被迭代。
__len__ 定義列舉實例的長度,即列舉成員的數量。
__contains__ 檢查列舉實例是否包含某個成員。
__getattribute__ 定義當存取列舉實例屬性時的行為。
__setattr__ 定義當設置列舉實例屬性時的行為。
__delattr__ 定義當刪除列舉實例屬性時的行為。

比較運算

列舉成員之間,原本是沒有比較關係的一組常數。我們可以透過上述 Dunder Methods 的實作,來讓他們可以彼此相互比較。

from enum import Enum

class Number(Enum):
    ONE = 1
    TWO = 2
    THREE = 3
    
    def __lt__(self, other):
        return isinstance(other, Number) and self.value < other.value
    
    def __eq__(self, other):
        if isinstance(other, Number):
            return self is other
        elif isinstance(other, int):
            return self.value == other
        else:
            return False

複習:使用 @total_ordering() 裝飾器來幫助比較大小的實作

from functools import total_ordering

@total_ordering()
class Number(Enum):

原本列舉是可雜湊(哈希)的,但我們實作上述比較方法後,就不再如此。

如果我們希望保持雜湊(哈希),可以透過實作 __hash__ 來達成本目的。

__bool__

介紹定制列舉,一定要提到 __bool__。因為和我們直覺的想像不同,如果不知道,很可能不小心就誤判。

所有列舉成員,預設值都是 True。

如果我們要對成員作 truthy 與否的判斷,請實作 __bool__ method。

延伸列舉

微動腦說明:Extend (subclass) our custom enumerations - but only under certain circumstances: as long as the enumeration we are extending does not define any members :

免動腦說明: Define an emum class with no members and methods and extends this base class.

因為延伸列舉的要求是:父類別中沒有成員,實作上通常是:

  1. 父類別:無成員,僅實作子類別會用到的 methods。

  2. 子類別:繼承後再加上成員。

We can add methods to our enumerations - this means we could define a base class that implements some common functionality for all our instances, and then extend this enumeration class to concrete(具體的) enumerations that define the members.

from enum import Enum

@total_ordering
class OrderedEnum(Enum):
    """Creates an ordering based on the member values. 
    So member values have to support rich comparisons.
    """
    
    def __lt__(self, other):
        if isinstance(other, OrderedEnum):
            return self.value < other.value
        return NotImplemented


class Number(OrderedEnum):
    ONE = 1
    TWO = 2
    THREE = 3
    
class Dimension(OrderedEnum):
    D1 = 1,
    D2 = 1, 1
    D3 = 1, 1, 1

HTTPStatus

一開始提過,PEP 435 中(適合使用列舉)的例子:錯誤值和狀態的定義(error status values and states within a defined process)。

from http import HTTPStatus

type(HTTPStatus)
# 輸出:enum.EnumMeta

list(HTTPStatus)[0:10]
# 以下為輸出:
[<HTTPStatus.CONTINUE: 100>,
 <HTTPStatus.SWITCHING_PROTOCOLS: 101>,
 <HTTPStatus.PROCESSING: 102>,
 <HTTPStatus.OK: 200>,
 <HTTPStatus.CREATED: 201>,
 <HTTPStatus.ACCEPTED: 202>,
 <HTTPStatus.NON_AUTHORITATIVE_INFORMATION: 203>,
 <HTTPStatus.NO_CONTENT: 204>,
 <HTTPStatus.RESET_CONTENT: 205>,
 <HTTPStatus.PARTIAL_CONTENT: 206>]

以下用 HTTPStatus 示範一些列舉值的呼叫方示。

# 續上述程式

HTTPStatus(200), HTTPStatus.OK, HTTPStatus.OK.name, HTTPStatus.OK.value
# 輸出:(<HTTPStatus.OK: 200>, <HTTPStatus.OK: 200>, 'OK', 200)

HTTPStatus.NOT_FOUND.value, HTTPStatus.NOT_FOUND.name, HTTPStatus.NOT_FOUND.phrase
# 輸出:(404, 'NOT_FOUND', 'Not Found')

AppStatus 仿作

完整的「錯誤值和狀態的定義」通常為完整實作 EnumMeta,由於 EnumMeta 不在老師規劃的課程範圍內,所以老師以實作 __new__ 示範作法。

補充:途中老師再次提醒 __new__ 的啟動時機,並以 print 證明。不過完整程式已移除這部分。

老師一開始純粹以 tuple 示範,但這種作法呼叫時要整個 tuple 放入,不夠直覺。

不建議的作法,我們就不放範例程式了。以下是建議的方式:

class AppStatus(Enum):
    OK = (0, 'No Problem!')
    FAILED = (1, 'Crap!')
    
    def __new__(cls, member_value, member_phrase):
        # create a new instance of cls
        member = object.__new__(cls)  # <========== 留意此處 object.__new__
        
        # set up instance attributes
        member._value_ = member_value  # <========== 留意此處 _value_
        member.phrase = member_phrase
        return member

AppStatus.OK.value, AppStatus.OK.name, AppStatus.OK.phrase
# 輸出:(0, 'OK', 'No Problem!')

AppStatus(0)
# 輸出:<AppStatus.OK: 0>

到目前為止,好像和延伸列舉沒有關係?改一下就有關係了喔:

class TwoValueEnum(Enum):
    def __new__(cls, member_value, member_phrase):
        member = object.__new__(cls)
        member._value_ = member_value
        member.phrase = member_phrase
        return member

class AppStatus(TwoValueEnum):
    OK = (0, 'No Problem!')
    FAILED = (1, 'Crap!')


AppStatus.FAILED, AppStatus.FAILED.name, AppStatus.FAILED.value, AppStatus.FAILED.phrase
# 輸出:(<AppStatus.FAILED: 1>, 'FAILED', 1, 'Crap!')

使用 延伸列舉 的做法,有以下優點(by ChatGPT):

  1. 可重用性: 新的寫法中,TwoValueEnum 是一個通用的基類,可以在其他需要相同創建和屬性設定邏輯的列舉類別中重複使用。這樣可以避免重複編寫相同的程式碼。

  2. 可擴展性: 使用中間基類的寫法可以方便地擴展列舉類別的功能。你可以在 TwoValueEnum 中添加額外的方法或屬性,並讓所有繼承自它的列舉類別都獲得這些功能。

  3. 代碼結構清晰: 新的寫法將創建和屬性設定的邏輯從具體的列舉類別中分離出來,使代碼結構更清晰和易於理解。每個類別只需要關注自身的列舉成員定義,而不需要擔心創建和屬性設定的細節。

總結起來,新的寫法通過引入中間基類,提供了更好的代碼組織結構、可重用性和擴展性,使列舉類別的定義更加清晰和彈性。

參考資料:

Automatic Values

有時我們建立列舉,其實只是要它的成員名,對其值並不在意(但值不能重覆,除了別名)。如果有個自動幫我們產生值的功能該多好…。

auto()

Python 的 enum 模組(module)中,auto() 是一個幫助類別(helper class)。

它的用途是在列舉(enumeration)中自動分配值給成員,這個功能對於需要自動產生連續或唯一值(會自動避開它之前產生的值,但不會避開你手動產生的值)的列舉項目很實用。

from enum import Enum, auto

class Weekday(Enum):
    MONDAY = auto()
    TUESDAY = auto()
    WEDNESDAY = auto()
    THURSDAY = auto()
    FRIDAY = auto()
    SATURDAY = auto()
    SUNDAY = auto()

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

_generate_next_value_()

執行 auto() 時,會呼叫 _generate_next_value_() function(static)。(所以 _generate_next_value_() 要放在 auto() 之前 )

顧名思義:產生下一個值。預設值是從 1 開始,每次加 1。

你可以改寫,以符合程式需求。

_generate_next_value_(name, start, count, last_values)

參數:

  1. name:字串,代表目前正在生成值的列舉項目的名稱。

  2. start:整數,代表第一個列舉項目的預設值。當你的列舉中沒有明確指定值時,會使用這個參數的預設值作為起始值。

  3. count:整數,代表已經定義的列舉項目數量。

  4. last_values:列表,包含已經生成的列舉項目的值。你可以根據這個列表來判斷前一個值,並在生成下一個值時進行適當的運算。

傳回值:下一個列舉項目的值。

這個值可以是任何符合你的需求的型別,例如整數、字串或其他自訂物件。

回傳的值應該是唯一的,以確保每個列舉項目都有獨特的值。你需要自己確保生成的值不會重複。

老師的建議: I never mix auto-generated values and my own - just to be on the safe side.

老師舉了混用可能會出問題的範例,這裡我就不放了。

from enum import Enum, auto

class Color(Enum):
    # 提醒:`_generate_next_value_` 必須在執行 auto() 前
    def _generate_next_value_(name, start, count, last_values):
        # 在這個範例中,我們返回每個顏色第一個字母的 ASCII 值作為其值
        return ord(name[0])

    RED = auto()
    GREEN = auto()
    BLUE = auto()

print(Color.RED.value)    # 印出 82
print(Color.GREEN.value)  # 印出 71
print(Color.BLUE.value)   # 印出 66

既然我們前面才剛介紹完延伸列舉,那就延伸列舉和 auto() 放在一起試試看吧。

class NameAsString(enum.Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name.lower()

class Enum1(NameAsString):
    A = enum.auto()
    B = enum.auto()
    
class Enum2(NameAsString):
    WAIT = enum.auto()
    RUNNING = enum.auto()
    FINISHED = enum.auto()

for member in Enum1:
    print(member.name, member.value)
#輸出:A a
#輸出:B b
    
for member in Enum2:
    print(member.name, member.value)
#輸出:WAIT wait
#輸出:RUNNING running
#輸出:FINISHED finished

關於 count

因為 count 計數包含別名,可以拿來避免不小心新建別名。

範例:以下示範當 count 是奇數時,設定其值為前一成員的值(所以會成為別名)。

class Aliased(enum.Enum):
    def _generate_next_value_(name, start, count, last_values):
        print(f'count={count}')
        if count % 2 == 1:
            # odd, make this member an alias of the previous one
            return last_values[-1]
        else:
            # make a new value
            return last_values[-1] + 1
       
    GREEN = 1
    GREEN_ALIAS = 1
    RED = 10
    CRIMSON = enum.auto()
    BLUE = enum.auto()
    AQUA = enum.auto()

# 以下為輸出:
count=3
count=4
count=5
# 續上方程式
list(Aliased)
# 輸出:[<Aliased.GREEN: 1>, <Aliased.RED: 10>, <Aliased.BLUE: 11>]

Aliased.__members__
# 以下為輸出:
mappingproxy({'GREEN': <Aliased.GREEN: 1>,
              'GREEN_ALIAS': <Aliased.GREEN: 1>,
              'RED': <Aliased.RED: 10>,
              'CRIMSON': <Aliased.RED: 10>,
              'BLUE': <Aliased.BLUE: 11>,
              'AQUA': <Aliased.BLUE: 11>})

參考資料:


延伸閱讀

  • Enumerations 列舉 的中文說明(3.10.11),建議使用 新同文堂 外掛 簡轉繁。
  • Enumerations 列舉 的英文說明(3.11.4),先用瀏覽器的英翻中,再用上述的新同文堂改為我們習慣的用詞。
  • Source code