什麼是列舉?
定義
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 if Color.RED in Color: |
比較是否同一成員 | is if Color.RED is Color.BLUE: |
比較成員值是否相等 | == 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 methodsand extends this base class.
因為延伸列舉的要求是:父類別中沒有成員,實作上通常是:
-
父類別:無成員,僅實作子類別會用到的 methods。
-
子類別:繼承後再加上成員。
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):
-
可重用性: 新的寫法中,
TwoValueEnum
是一個通用的基類,可以在其他需要相同創建和屬性設定邏輯的列舉類別中重複使用。這樣可以避免重複編寫相同的程式碼。 -
可擴展性: 使用中間基類的寫法可以方便地擴展列舉類別的功能。你可以在
TwoValueEnum
中添加額外的方法或屬性,並讓所有繼承自它的列舉類別都獲得這些功能。 -
代碼結構清晰: 新的寫法將創建和屬性設定的邏輯從具體的列舉類別中分離出來,使代碼結構更清晰和易於理解。每個類別只需要關注自身的列舉成員定義,而不需要擔心創建和屬性設定的細節。
總結起來,新的寫法通過引入中間基類,提供了更好的代碼組織結構、可重用性和擴展性,使列舉類別的定義更加清晰和彈性。
參考資料:
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)
參數:
-
name
:字串,代表目前正在生成值的列舉項目的名稱。 -
start
:整數,代表第一個列舉項目的預設值。當你的列舉中沒有明確指定值時,會使用這個參數的預設值作為起始值。 -
count
:整數,代表已經定義的列舉項目數量。 -
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