Python 列舉介紹,以及定義和使用 Python 列舉

閱讀 22:02·字數 6614·更新 
Youtube 頻道
訂閱 133

先決條件

閱讀本節的先決條件是對 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 列舉成員的值。

enumerations.py
from enum import *

class SPORT(Enum): '''一個關於運動的列舉,包含了籃球,足球和棒球''' FOOTBALL = 1 BASKETBALL = 2 BASEBALL = 3

除了通過直接定義類別,Python 還支援通過Enum類別或其衍生類別(FlagStrEnumIntEnum等)的建構子來建立新的列舉,其基本格式如下。

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,因此列舉成員的值需要是字串。

enumerations.py
# 定義列舉 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 列舉成員在語法形式上與類別變數類似,但他們是一種語法糖,列舉成員將是唯讀,不可寫入的,每一個列舉成員都是列舉的一個執行個體,成員的型別是列舉自身,這一點可通過isinstancetype來驗證。

enumerations.py
print(type(SPORT.FOOTBALL))
# 判斷成員 FOOTBALL 是否為 SPORT 的執行個體
print(isinstance(SPORT.FOOTBALL, SPORT))
<enum 'SPORT'>
True

Python 列舉成員的值可以是任意型別

這不同於我們對列舉的一般印象,因為很多語言中的列舉都和數值緊密相關,但 Python 並未對列舉成員值的型別施加限製,如果列舉沒有指定混合型別,也沒有從某些特定的類別繼承的話,比如IntEnumStrEnumIntFlag

官方並不推薦使用IntEnumIntFlag等類別,對列舉成員的值的型別進行約束,因為這將導致定義的 Python 列舉不符合一般約定。比如,從IntEnum衍生的列舉可以與整數進行有效比較,這不應該發生。

使用 @global_enum 修飾詞將 Python 列舉成員定義至模組所在的命名空間

如果為 Python 列舉新增enum模組中的@global_enum修飾詞,那麽該列舉的成員將出現在模組所在的命名空間,你不再需要通過列舉來存取他們。

enumerations.py
# 列舉成員 A 和 B 將出現在模組中
@global_enum
class LETTER(Enum):
	A = 'a'
	B = 'b'

# 下面的存取方式都是正確的 print(LETTER.A) print(A)
A
A

使用 @verify 修飾詞確保 Python 列舉成員值的連貫性

對於整數型別的 Python 列舉成員,如果你希望他們的值具有連貫性,比如從19,那麽可以使用enum模組的CONTINUOUS@verify修飾詞對列舉進行限製,如果列舉成員中的最大值與最小值之間存在未被使用的值,則將引發例外狀況ValueError

由於下面的列舉NUM未定義值為2的列舉成員,因此將引發例外狀況。

enumerations.py
# 列舉 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_對應的類別變數指派,不會達到預期效果。

enumerations.py
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的別名。

enumerations.py
class SPORT(Enum):
	# …
	SOCCER = 1

print(SPORT.SOCCER) print(SPORT.SOCCER.name)
SPORT.FOOTBALL
FOOTBALL

作為別名的 Python 列舉成員不會出現在對列舉的疊代周遊中

對一個 Python 列舉進行疊代周遊,你將得到該列舉所有未作為別名的列舉成員,這是較為合理的,因為包含別名可能會導致在疊代周遊中執行多余的操作。

enumerations.py
# 這裏不會出現 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模組。

enumerations.py
# 可以將 @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__特性,該特性是一個對映型別物件,包含了全部列舉成員的資訊,其鍵值組的鍵為列舉成員的名稱,值為列舉成員的值。

enumerations.py
# 顯示 SPORT 的全部成員,包括 SOCCER
print(SPORT.__members__)
{'FOOTBALL': <SPORT.FOOTBALL: 1>, 'BASKETBALL': <SPORT.BASKETBALL: 2>, 'BASEBALL': <SPORT.BASEBALL: 3>, 'SOCCER': <SPORT.FOOTBALL: 1>}

取得 Python 列舉成員的名稱和值

Python 列舉成員擁有兩個唯讀屬性(Property)namevalue,他們分別表示了 Python 列舉成員的名稱和值,比如,SPORT.BASEBALL.value將傳回3

取得 Python 列舉中定義的列舉成員的個數

通過len函式,你可以得到 Python 列舉所定義的列舉成員的個數。

enumerations.py
# 取得列舉 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 列舉,那麽之前保留的列舉成員不會與新的列舉成員相等,因為重新定義導致這些列舉成員成為了新的列舉執行個體。

enumerations.py
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模組的IntEnumIntFlag類別的基底類別包括int,因此從IntEnumIntFlag類別衍生的 Python 列舉,並不遵守以上關於比較的規則,他們可以與某些數值進行有效的比較。假設將列舉SPORT改為從IntEnum類別衍生,那麽運算式SPORT.FOOTBALL==1將傳回True,而不是False,運算式SPORT.FOOTBALL>=0將傳回True,而不是引發例外狀況。

Python 列舉成員的值

在預設情況下,Python 列舉成員的值就是書寫在=後面的運算式所傳回的內容。如果你希望對這些內容施加新規則,從而產生新的值,那麽可以通過定義__new__方法來完成該任務,=後的運算式的運算結果會作為該方法的參數。當然,__new__方法需要傳回一個列舉執行個體,而新的值應被儲存在執行個體的_value_變數中。

Python 列舉成員的 _value_ 變數

Python 列舉成員的_value_變數,用於實際儲存列舉成員的值,該值也可被列舉成員的唯讀屬性value傳回。

在下面的程式碼中,我們使用元組為列舉SPEED的成員LOWHIGH指派,__new__方法會計算元組中的數值的和,並將其作為列舉成員的值。

enumerations.py
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ⁿn0開始計算。

如何設定 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中,成員UPRIGHT的值分別為12,成員LEFT的值為101,因為第三個auto()會根據DOWN對應的100來決定傳回值。

enumerations.py
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()的傳回值,它是一個字串,包含了列舉成員的名稱和一個數值。

enumerations.py
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表示列舉成員。

enumerations.py
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

enumerations.py
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繼承的FlagIntFlagIntEnumStrEnum等類別,他們均可作為其他列舉的基底類別。

IntEnum 類別

IntEnum類別繼承的列舉的成員的值必須是整數型別,=後的運算式的傳回值將用於建立整數。IntEnum類別的基底類別包括Enumint,因此他同時具有兩者的特點,可以與其他數值進行算術運算,有效比較,或出現在任何允許使用整數的位置。當參與算術運算時,運算結果將不再是某個列舉成員。

StrEnum 類別

StrEnum類別繼承的列舉的成員的值必須是字串型別。StrEnum類別的基底類別包括Enumstr,因此他同時具有兩者的特點,可以出現在允許使用字串的位置。

ReprEnum 類別

ReprEnum類別繼承的 Python 列舉必須指定一種型別(比如intstrfloat)作為列舉的基底類別,以確定列舉成員值的型別,當你為成員指定其他型別的值時,將發生隱含轉換。

Flag,IntFlag 類別

FlagIntFlag類別繼承的列舉也被稱為 Python 旗標,他將支援列舉成員之間的組合操作。

旗標

想要詳細了解 Python 旗標,你可以檢視如何定義和使用支援成員組合的 Python 列舉?Python 旗標介紹一節。

列舉JOB的成員DOCTOR,使用元組'100',8來建立整數。列舉HERO的成員TOM,會將整數123轉換為字串'123'。列舉TIME的成員MINUTE將導致例外狀況TypeError,因為StrEnum不支援非字串型別的轉換。

enumerations.py
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

內容分類

程式碼

enumerations.py·codebeatme/python·GitHub