Python 列舉介紹,以及定義和使用 Python 列舉
先決條件
閱讀本節的先決條件是對 Python 類別的繼承以及列舉的概念有所掌握,你可以檢視Python 類別繼承介紹,以及實作 Python 類別繼承,多重繼承,方法覆寫,程式設計教學的列舉介紹,以及列舉成員的組合,判斷,移除來了解相關資訊。
定義 Python 列舉
Python 並沒有提供類似於enum
這樣的關鍵字來定義列舉,你需要從enum
模組的Enum
類別或其衍生類別,衍生新的類別作為 Python 列舉,其基本語法格式如下。
class <enumname>(Enum):
<membername> = <membervalue>
…
- enumname 部分
enumname
為 Python 列舉的名稱,需要符合 Python 識別碼規格。- membername 部分
membername
為 Python 列舉成員的名稱,需要符合 Python 識別碼規格,建議所有字母均大寫。- membervalue 部分
membervalue
是一個運算式,用於計算 Python 列舉成員的值。
from enum import *
class SPORT(Enum):
'''一個關於運動的列舉,包含了籃球,足球和棒球'''
FOOTBALL = 1
BASKETBALL = 2
BASEBALL = 3
除了通過直接定義類別,Python 還支援通過Enum
類別或其衍生類別(Flag
,StrEnum
,IntEnum
等)的建構子來建立新的列舉,其基本格式如下。
Enum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
- value 參數
value
參數為 Python 列舉的名稱。- names 參數
names
參數包含了所有列舉成員的資訊,該參數可以是一個僅包含成員名稱的字串,名稱之間使用空格(,
)分隔,比如'ONE,TWO'
,也可以是一個包含成員名稱或名稱和值的疊代器物件,比如['ONE','TWO']
,(('ONE',100),('TWO',200))
,還可以是一個包含成員名稱和值的對映物件,比如{'ONE':100,'TWO':200}
。- module 參數
module
參數將成為列舉的__module__
特性的值,表示 Python 列舉所在的模組的名稱。- qualname 參數
qualname
參數將成為列舉的__qualname__
特性的值,表示 Python 列舉在模組中的完整名稱,比如,World.PLANT
說明列舉PLANT
定義在模組的World
中,World
可能是一個類別。- type 參數
type
參數為 Python 列舉的混合型別。- start 參數
start
參數為 Python 列舉成員的起始值,由auto
類別使用。- boundary 參數
boundary
參數表示對超出範圍的值的處理方式。
下面,我們使用StrEnum
的建構子定義了新的列舉PLANT
,由於是StrEnum
,因此列舉成員的值需要是字串。
# 定義列舉 PLANT
PLANT = StrEnum('PLANT', (['TREE', 'one tree'], ['FLOWER', 'one flower']))
print(PLANT.__members__)
{'TREE': <PLANT.TREE: 'one tree'>, 'FLOWER': <PLANT.FLOWER: 'one flower'>}
Python 列舉的成員
Python 列舉成員(Member)是以類別變數形式出現的特性(Attribute),與類別變數一樣,列舉成員擁有成員名稱和值。比如,範例中列舉SPORT
的成員FOOTBALL
的名稱為FOOTBALL
,值為1
。
雖然 Python 列舉成員在語法形式上與類別變數類似,但他們是一種語法糖,列舉成員將是唯讀,不可寫入的,每一個列舉成員都是列舉的一個執行個體,成員的型別是列舉自身,這一點可通過isinstance
和type
來驗證。
print(type(SPORT.FOOTBALL))
# 判斷成員 FOOTBALL 是否為 SPORT 的執行個體
print(isinstance(SPORT.FOOTBALL, SPORT))
<enum 'SPORT'>
True
Python 列舉成員的值可以是任意型別
這不同於我們對列舉的一般印象,因為很多語言中的列舉都和數值緊密相關,但 Python 並未對列舉成員值的型別施加限製,如果列舉沒有指定混合型別,也沒有從某些特定的類別繼承的話,比如IntEnum
,StrEnum
,IntFlag
。
官方並不推薦使用IntEnum
,IntFlag
等類別,對列舉成員的值的型別進行約束,因為這將導致定義的 Python 列舉不符合一般約定。比如,從IntEnum
衍生的列舉可以與整數進行有效比較,這不應該發生。
使用 @global_enum 修飾詞將 Python 列舉成員定義至模組所在的命名空間
如果為 Python 列舉新增enum
模組中的@global_enum
修飾詞,那麽該列舉的成員將出現在模組所在的命名空間,你不再需要通過列舉來存取他們。
# 列舉成員 A 和 B 將出現在模組中
@global_enum
class LETTER(Enum):
A = 'a'
B = 'b'
# 下面的存取方式都是正確的
print(LETTER.A)
print(A)
A
A
使用 @verify 修飾詞確保 Python 列舉成員值的連貫性
對於整數型別的 Python 列舉成員,如果你希望他們的值具有連貫性,比如從1
到9
,那麽可以使用enum
模組的CONTINUOUS
和@verify
修飾詞對列舉進行限製,如果列舉成員中的最大值與最小值之間存在未被使用的值,則將引發例外狀況ValueError
。
由於下面的列舉NUM
未定義值為2
的列舉成員,因此將引發例外狀況。
# 列舉 NUM 的成員的值需要是連貫的
@verify(CONTINUOUS)
class NUM(Enum):
ONE = 1
FOUR = 4
THREE = 3
ValueError: invalid enum 'NUM': missing values 2
Python 列舉成員的名稱不能與其他列舉成員或特性的名稱相同
在一般的 Python 類別中,定義名稱相同的特性,比如名稱相同的類別變數和執行個體方法,並不會產生語法上的錯誤,但這樣的行為對於 Python 列舉成員是行不通的,列舉成員的名稱不應該與其他列舉成員或特性的名稱相同,這會導致例外狀況TypeError
。
將類別變數從 Python 列舉成員中排除
除了私用類別變數,名稱以單底線(_
)開頭和結束,以及名稱存在於_ignore_
中的類別變數,Python 列舉中的其他類別變數都將被解釋為列舉成員。
如果希望某個類別變數從列舉成員中排除,那麽可以使用enum
模組的nonmember
類別為類別變數指派。當然,你也可以使用具有相反效果的member
類別為類別變數指派,他將使類別變數成為 Python 列舉成員,只不過,這樣的操作基本上是多余的。
另外,可以將不希望解釋為列舉成員的類別變數名稱,包含在類別變數_ignore_
中,該變數可以是字串,串列或元組物件。如果是字串,那麽多個變數名稱需要使用空格(
)或逗號(,
)分隔。需要指出的是,_ignore_
應該定義在其對應的類別變數之前,否則將導致例外狀況ValueError
,使用member
類別為_ignore_
對應的類別變數指派,不會達到預期效果。
class POSITION(Enum):
_ignore_ = ('Z', 'W')
# X 不是列舉成員
X = nonmember(1)
# Y 是列舉成員
Y = 2
# Z 和 W 不是列舉成員,雖然 W 使用了 member
Z = 3
W = member(4)
print(POSITION.__members__)
{'Y': <POSITION.Y: 2>}
Python 列舉成員的別名
在一個 Python 列舉中,當多個列舉成員擁有相同的名稱時,之後定義的成員的名稱,將成為首先定義的成員的別名。當你嘗試取得之後定義的 Python 列舉成員時,傳回的將是首先定義的列舉成員,這些列舉成員均指向列舉的同一個執行個體。
下面,我們為列舉SPORT
新增一個值為1
的列舉成員SOCCER
,他的名稱SOCCER
將成為列舉成員FOOTBALL
的別名。
class SPORT(Enum):
# …
SOCCER = 1
print(SPORT.SOCCER)
print(SPORT.SOCCER.name)
SPORT.FOOTBALL
FOOTBALL
作為別名的 Python 列舉成員不會出現在對列舉的疊代周遊中
對一個 Python 列舉進行疊代周遊,你將得到該列舉所有未作為別名的列舉成員,這是較為合理的,因為包含別名可能會導致在疊代周遊中執行多余的操作。
# 這裏不會出現 SOCCER,因為他是 FOOTBALL 的別名
for i in SPORT:
print(f'{i.name}={i.value}')
FOOTBALL=1
BASKETBALL=2
BASEBALL=3
使用 @unique 或 @verify 修飾詞確保 Python 列舉成員值的不重複
如果不希望別名的情況發生,可以為 Python 列舉新增enum
模組的@unique
或@verify
修飾詞,他們將確保列舉中的每一個列舉成員都擁有不同的值。其中,@verify
修飾詞需要書寫為@verify(UNIQUE)
,UNIQUE
位於enum
模組。
# 可以將 @verify(UNIQUE) 取代為 @unique
@verify(UNIQUE)
class ROLE(Enum):
PLAYER = 1
# ERROR 這裏不能出現別名
HERO = 1
ValueError: aliases found in <enum 'ROLE'>: HERO -> PLAYER
取得 Python 列舉成員
有多種方式可以取得 Python 列舉中的某個指定成員,最為常見的是通過形式為m.name
的運算式,其中,m
為 Python 列舉類別,name
為列舉成員(名稱,識別碼)。比如,SPORT.BASEBALL
取得了列舉SPORT
的成員BASEBALL
。
除了.
,你還可以通過[]
取得某個 Python 列舉成員,只需給出成員的名稱即可,如果無法確定想要取得的列舉成員,那麽使用[]
是一個不錯的選擇。假設,變數name
含有某個成員的名稱,SPORT[name]
將傳回該名稱對應的 Python 列舉成員。
如果只知道某個 Python 列舉成員的值,那麽通過該列舉的建構子可取得對應的列舉成員。比如,SPORT(2)
將傳回列舉SPORT
的成員BASKETBALL
。
Python 列舉支援疊代周遊操作,你將得到所有未作為別名的列舉成員。如果要取得包括別名在內的全部成員,則可以使用 Python 列舉的__members__
特性,該特性是一個對映型別物件,包含了全部列舉成員的資訊,其鍵值組的鍵為列舉成員的名稱,值為列舉成員的值。
# 顯示 SPORT 的全部成員,包括 SOCCER
print(SPORT.__members__)
{'FOOTBALL': <SPORT.FOOTBALL: 1>, 'BASKETBALL': <SPORT.BASKETBALL: 2>, 'BASEBALL': <SPORT.BASEBALL: 3>, 'SOCCER': <SPORT.FOOTBALL: 1>}
取得 Python 列舉成員的名稱和值
Python 列舉成員擁有兩個唯讀屬性(Property)name
和value
,他們分別表示了 Python 列舉成員的名稱和值,比如,SPORT.BASEBALL.value
將傳回3
。
取得 Python 列舉中定義的列舉成員的個數
通過len
函式,你可以得到 Python 列舉所定義的列舉成員的個數。
# 取得列舉 NUM 中已經定義的成員的個數
print(f'NUM 成員的個數為:{len(NUM)}')
NUM 成員的個數為:4
比較 Python 列舉成員
在某些程式設計語言中,列舉可以有效的與整數型別進行比較操作,這帶來了方便,但也會使程式碼的可讀性降低。
Python 中的列舉成員與任何非列舉成員總是不相等的,比如,運算式SPORT.FOOTBALL==1
的運算結果為False
,雖然列舉SPORT
的成員FOOTBALL
的值(SPORT.FOOTBALL.value
)是1
。Python 列舉成員,以及列舉成員與非列舉成員之間,不能進行大於(>
),小於(<
),大於等於(>=
),小於等於(<=
)的比較,比如,運算式SPORT.FOOTBALL>=0
將導致例外狀況TypeError
。
正如之前講到的,Python 列舉成員是列舉的執行個體,因此,Python 列舉成員之間的比較,也就是執行個體之間的比較。當他們是同一個執行個體時,is
運算會傳回True
,否則會傳回False
,比如,運算式SPORT.FOOTBALL is SPORT.BASKETBALL
的運算結果為False
。
重新定義 Python 列舉會導致前後的同一個列舉成員不相等
如果你重新定義了 Python 列舉,那麽之前保留的列舉成員不會與新的列舉成員相等,因為重新定義導致這些列舉成員成為了新的列舉執行個體。
football = SPORT.FOOTBALL
# 重新定義列舉 SPORT
class SPORT(Enum):
FOOTBALL = 1
BASKETBALL = 2
BASEBALL = 3
SOCCER = 1
# 比較之前和新的列舉成員 FOOTBALL,傳回 False
print(football == SPORT.FOOTBALL)
False
繼承自 IntEnum,IntFlag 的 Python 列舉的比較問題
由於enum
模組的IntEnum
,IntFlag
類別的基底類別包括int
,因此從IntEnum
,IntFlag
類別衍生的 Python 列舉,並不遵守以上關於比較的規則,他們可以與某些數值進行有效的比較。假設將列舉SPORT
改為從IntEnum
類別衍生,那麽運算式SPORT.FOOTBALL==1
將傳回True
,而不是False
,運算式SPORT.FOOTBALL>=0
將傳回True
,而不是引發例外狀況。
Python 列舉成員的值
在預設情況下,Python 列舉成員的值就是書寫在=
後面的運算式所傳回的內容。如果你希望對這些內容施加新規則,從而產生新的值,那麽可以通過定義__new__
方法來完成該任務,=
後的運算式的運算結果會作為該方法的參數。當然,__new__
方法需要傳回一個列舉執行個體,而新的值應被儲存在執行個體的_value_
變數中。
Python 列舉成員的 _value_ 變數
Python 列舉成員的_value_
變數,用於實際儲存列舉成員的值,該值也可被列舉成員的唯讀屬性value
傳回。
在下面的程式碼中,我們使用元組為列舉SPEED
的成員LOW
和HIGH
指派,__new__
方法會計算元組中的數值的和,並將其作為列舉成員的值。
class SPEED(int, Enum):
def __new__(cls, *args):
# 計算所有數值的和,並儲存至 total
total = 0
for i in args:
total += i
# 建立列舉的執行個體,並將之前的計算結果指派給 _value_
prop = int.__new__(cls)
prop._value_ = total
return prop
LOW = (1, 2, 3, 4, 5)
HIGH = (6, 7, 8, 9)
print(SPEED.LOW.value)
15
使用 auto 類別自動設定 Python 列舉成員的值
Python 的enum
模組提供了名為auto
的類別,可以實作 Python 列舉成員值的自動設定,只需要簡單的在=
之後書寫auto()
即可。
在預設情況下,auto()
會傳回一個可用的遞增的整數,從1
開始計算。如果 Python 列舉從StrEnum
類別衍生,那麽auto()
會傳回成員名稱的小寫字串。如果 Python 列舉從Flag
類別衍生,那麽auto()
會傳回二的整數次冪2ⁿ
,n
從0
開始計算。
如何設定 auto() 為 Python 列舉成員傳回的成員值?
當你使用修飾詞@staticmethod
在列舉中定義靜態方法_generate_next_value_
時,該靜態方法的傳回值可作為auto()
的傳回值。
需要指出,_generate_next_value_
方法需要定義在所有使用auto()
指派的列舉成員之前,否則將引發例外狀況TypeError
。
_generate_next_value_(name, start, count, last_values)
- name 參數
name
參數為目前正在定義的 Python 列舉成員的名稱。- start 參數
start
參數為 Python 列舉成員的預期起始值,預設為1
。- count 參數
count
參數為已經定義的 Python 列舉成員的個數,不包括目前正在定義的成員。- last_values 參數
last_values
是一個 Python 串列,包含了之前所有已定義列舉成員的值。
在下面的列舉DIRECTION
中,成員UP
和RIGHT
的值分別為1
和2
,成員LEFT
的值為101
,因為第三個auto()
會根據DOWN
對應的100
來決定傳回值。
class DIRECTION(Enum):
UP = auto()
RIGHT = auto()
DOWN = 100
# 將根據 100 傳回 101
LEFT = auto()
print(f'{DIRECTION.UP.value} {DIRECTION.RIGHT.value} {DIRECTION.DOWN.value} {DIRECTION.LEFT.value}')
1 2 100 101
為列舉DIRECTION
加入靜態方法_generate_next_value_
,以控製auto()
的傳回值,它是一個字串,包含了列舉成員的名稱和一個數值。
class DIRECTION(Enum):
# 需要定義在所有列舉成員之前
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return f'{name}_{count}'
# …
UP_0 RIGHT_1 100 LEFT_3
Python 列舉的方法
Python 列舉可使用修飾詞@classmethod
,@staticmethod
來定義類別方法或靜態方法,通過列舉或列舉成員可以呼叫他們。對於 Python 列舉中定義的執行個體方法,其第一參數self
表示列舉成員。
class DAY(str, ReprEnum):
MONDAY = 1
TUESDAY = 2
# self 代表了列舉成員
def show(self):
print(f'{self.name}={self.value}')
DAY.TUESDAY.show()
TUESDAY=2
超出取值範圍的 Python 列舉成員
你可以在 Python 列舉中定義名稱為_missing_
的類別方法,用於處理列舉成員的取值超出範圍的情況,比如,為列舉的建構子傳遞了一個值,該值沒有對應任何一個列舉成員。
如果_missing_
方法最終傳回了 Python 列舉的某個成員,那麽該成員將作為值對應的列舉成員,如果_missing_
方法傳回了None
,那麽將引發例外狀況,已說明取值是無效的。
_missing_(cls, value)
- cls 參數
cls
參數為 Python 列舉。- value 參數
value
為超出範圍的取值。
在列舉ANIMAL
中,我們定義了類別方法_missing_
,已處理超出範圍的取值,如果取值為小於100
的整數,則將DOG
作為其對應的列舉成員,如果取值為大於100
小於200
的整數,則將CAT
作為其對應的列舉成員,其他情況傳回None
。
class ANIMAL(Enum):
DOG = 100
CAT = 200
# 處理超出取值範圍的列舉成員
@classmethod
def _missing_(cls, value):
if type(value) is int:
if value < 100:
# 小於 100 的整數,將被認為是 DOG
return ANIMAL.DOG
elif value < 200:
# 大於 100 小於 200 的整數,將被認為是 CAT
return ANIMAL.CAT
# 其余取值傳回 None,這將導致例外狀況的發生
return None
print(ANIMAL(99))
print(ANIMAL(111))
ANIMAL.DOG
ANIMAL.CAT
繼承 Python 列舉
Python 允許從一個列舉衍生另一個列舉,先決條件是作為基底類別的列舉尚未擁有任何列舉成員。enum
模組自身包含了從Enum
繼承的Flag
,IntFlag
,IntEnum
,StrEnum
等類別,他們均可作為其他列舉的基底類別。
- IntEnum 類別
從
IntEnum
類別繼承的列舉的成員的值必須是整數型別,=
後的運算式的傳回值將用於建立整數。IntEnum
類別的基底類別包括Enum
和int
,因此他同時具有兩者的特點,可以與其他數值進行算術運算,有效比較,或出現在任何允許使用整數的位置。當參與算術運算時,運算結果將不再是某個列舉成員。- StrEnum 類別
從
StrEnum
類別繼承的列舉的成員的值必須是字串型別。StrEnum
類別的基底類別包括Enum
和str
,因此他同時具有兩者的特點,可以出現在允許使用字串的位置。- ReprEnum 類別
從
ReprEnum
類別繼承的 Python 列舉必須指定一種型別(比如int
,str
,float
)作為列舉的基底類別,以確定列舉成員值的型別,當你為成員指定其他型別的值時,將發生隱含轉換。- Flag,IntFlag 類別
從
Flag
或IntFlag
類別繼承的列舉也被稱為 Python 旗標,他將支援列舉成員之間的組合操作。
旗標
想要詳細了解 Python 旗標,你可以檢視如何定義和使用支援成員組合的 Python 列舉?Python 旗標介紹一節。
列舉JOB
的成員DOCTOR
,使用元組'100',8
來建立整數。列舉HERO
的成員TOM
,會將整數123
轉換為字串'123'
。列舉TIME
的成員MINUTE
將導致例外狀況TypeError
,因為StrEnum
不支援非字串型別的轉換。
class JOB(IntEnum):
WORKER = 1
DOCTOR = '100', 8
# 參與算術運算後,運算結果不再是某個列舉成員
print(f'{type(JOB.WORKER)} {type(JOB.DOCTOR)}')
print(type(JOB.WORKER + 3))
print(type(JOB.WORKER + JOB.DOCTOR))
class HERO(str, ReprEnum):
# 123 會被轉換為 '123'
TOM = 123
JERRY = 'mouse?'
class TIME(StrEnum):
SECOND = 'sec'
# ERROR 1 並不會轉換為 '1'
MINUTE = 1
<enum 'JOB'> <enum 'JOB'>
<class 'int'>
<class 'int'>
…
TypeError: 1 is not a string