前情提示: 测试代码中,右尖括号( >
)表示命令行中输入的命令; 单独一行并以井字符( #
)开头的为输出内容; 库的导入仅在本文的第一个测试代码中展现,其他代码块均省略库的导入代码。
-
系统类型
:Windows 10
-
python 版本
:Python 3.9.0
枚举是一组符号名称(枚举成员)的集合,枚举成员应该是唯一的、不可变的。
enum
模块将分为三个部分解析,第一部分主要介绍枚举的特性和 Enum
类,第二部分将介绍 Enum
类的三个变种类及自定义类,第三部分将更深入的了解枚举。也可通过传送门查看微信公众号文章。
传送门
第一部分
创建一个 Enum
枚举是使用 class
语法来创建的,这使得它们易于读写。但还是有一种以使用函数形式的创建方式,这个留到下文再讲,现在主要来说明一下使用 class
语法的创建方式。
import enum class Test(enum.Enum): A = 1 B = 'b' 复制代码
上面的代码创建了一个枚举, class Test
是一个 枚举
,其中 Test
就是枚举对象的名称。
A = 1
、 B = 'b'
这些等号两边的整体被称为 枚举成员
,等号左边的被称为枚举成员的名称,枚举通常是用来表示常量的,所以枚举成员的名称一般使用大写形式。等号右边的被称为枚举成员的值,枚举成员的值可以是任意类型,如 int
、 str
等等,只需要注意枚举成员的值在 枚举对象中是唯一的。
虽然创建枚举时使用了 class
语法,但是枚举与普通的 python
类不同,可以打印一些信息来进行对比:
'''枚举''' class Test(enum.Enum): A = 1 print(Test) # 打印枚举 # <enum 'Test'> print(Test.A) # 打印枚举成员本身 # Test.A print(repr(Test.A)) # 打印枚举成员在解释器中的形式 # <Test.A: 1> print(type(Test.A)) # 打印枚举成员类型 # <enum 'Test'> print(isinstance(Test.A, Test)) # 判断枚举成员类型是否有枚举类型一致 # True '''普通的 python 类''' class Common: A = 1 print(Common) # 打印类 # <class '__main__.Common'> print(Common.A) # 打印类属性的值 # 1 print(repr(Common.A)) # 打印类属性的值在解释器中的形式 # 1 print(type(Common.A)) # 打印类属性的值的类型 # <class 'int'> print(isinstance(Common.A, Test)) # 判断类属性的值的类型是否有类的类型一致 # False 复制代码
首先,枚举创建出的对象自成一种类型,类型名就是枚举的名称(类名)。枚举的枚举成员(类的属性)被创建成了一个对象,并且类型就是所属枚举的类型。
枚举可以按照定义顺序进行迭代,并且枚举成员是不可变的,是可哈希的,因此字典中的键可以使用枚举成员。
class Test(enum.Enum): A = 1 test_dict = {} for i in Test: test_dict[i] = 'value.' + str(i) print(test_dict) # {<Test.A: 1>: 'value.Test.A', <Test.B: 'b'>: 'value.Test.B', <Test.C: 'c'>: 'value.Test.C'} print(test_dict[Test.A]) # value.Test.A 复制代码
那么怎么获取枚举成员呢,在枚举中有很多种访问方式:
class Test(enum.Enum): A = 1 print(Test(1)) # 程序化访问 # Test.A print(Test['A']) # 条目访问 # Test.A print(Test.A.value) # 枚举成员的 value 属性 # 1 print(Test.A.name) # 枚举成员的 name 属性 # A 复制代码
枚举中不允许有相同的枚举成员,但是允许枚举成员有同样的值,若查找的值有多个枚举成员,那么会按定义顺序返回第一个。
'''枚举成员有相同的名称''' class Test(enum.Enum): A = 1 A = 2 # TypeError: Attempted to reuse key: 'A' '''枚举成员有相同的值''' class Test(enum.Enum): A = 1 B = 1 print(Test(1)) # Test.A 复制代码
enum
模块中定义了一个装饰器 — @enum.unique
,可以强制规定枚举成员的值也必须是唯一的。
@enum.unique class Test(enum.Enum): A = 1 B = 1 # ValueError: duplicate values found in <enum 'Test'>: B -> A 复制代码
若枚举成员的值不重要或者其他不需要一一指定枚举成员的场景下,可以使用 enum.auto()
方法来替代输入的值。
class Test(enum.Enum): A = enum.auto() B = enum.auto() C = 1 print(repr(Test.A)) # <Test.A: 1> print(repr(Test.B)) # <Test.B: 2> print(repr(Test.C)) # <Test.A: 1> print(list(Test)) # [<Test.A: 1>, <Test.B: 2>] 复制代码
当使用 enum.auto()
来定义枚举成员的值时,再次使用 int
类型的值时,最后得到的结果可能会和预想的不太一样。
enum.auto()
函数的返回值是由 _generate_next_value_()
函数决定的,默认情况下,此函数是根据最后一个 int
类型的枚举成员的值增加 1
。此函数是可以重载的,重载时,方法定义必须在任何成员之前。
'''先定义一个值为 int 类型的枚举成员, 再使用 auto() 函数''' class Test(enum.Enum): A = 2 B = enum.auto() C = enum.auto() print(list(Test)) # [<Test.A: 2>, <Test.B: 3>, <Test.C: 4>] '''定义一个值为非 int 类型的枚举成员, 再使用 auto() 函数''' class Test(enum.Enum): A = 'b' B = enum.auto() C = enum.auto() print(list(Test)) # [<Test.A: 'b'>, <Test.B: 1>, <Test.C: 2>] '''重载 _generate_next_value_()''' class Test(enum.Enum): def _generate_next_value_(name, start, count, last_values): return name # 返回枚举成员的名字 A = enum.auto() B = enum.auto() print(list(Test)) # [<Test.A: 'A'>, <Test.B: 'B'>] 复制代码
在介绍 enum.auto()
函数时,测试示例中,枚举对象转换为 list
类型时少了一个枚举成员,这是因为少的那个枚举成员与之前的枚举成员的值是一样的,这个枚举成员被认为是一个别名,当对枚举成员迭代时,不会给出别名。
class Test(enum.Enum): A = 1 B = 1 print(list(Test)) # [<Test.A: 1>] 复制代码
而特殊属性 __members__
是一个从名称到成员的只读有序映射。它包含枚举中所有的枚举成员,但是在迭代时,因为别名的原因,指向的枚举成员还是该值第一个定义的枚举成员。
class Test(enum.Enum): A = 1 B = 1 print(dict(Test.__members__)) # {'A': <Test.A: 1>, 'B': <Test.A: 1>} 复制代码
小栗子,找出枚举中所有的别名:
class Test(enum.Enum): A = 1 B = 1 C = 2 D = 2 print([名称 for 名称, 枚举成员 in Test.__members__.items() if 枚举成员.name != 名称]) # ['B', 'D'] 复制代码
在枚举中,不仅仅只可以定义一些枚举成员,枚举也属于 python
的类,是可以拥有普通方法和特殊方法的,这里列举一个文档上的示例:
class Mood(enum.Enum): FUNKY = 1 HAPPY = 3 def describe(self): return self.name, self.value def __str__(self): return 'my custom str! {0}'.format(self.value) @classmethod def favorite_mood(cls): return cls.HAPPY print(Mood.favorite_mood()) # my custom str! 3 print(Mood.HAPPY.describe()) # ('HAPPY', 3) print(str(Mood.FUNKY)) # my custom str! 1 复制代码
但是枚举还是有一些限制,以单下划线开头和结尾的名称是由枚举保留而不可使用,例外项是包括特殊方法成员 ( __str__()
、 __add__()
等),描述符 (方法也属于描述符) 以及在 _ignore_
中列出的变量名。
在介绍 _generate_next_value_()
函数时,为了重载这个函数,我们创建了一个基于 enum.Enum
的枚举,可以成为 枚举A
,重载了 _generate_next_value_()
函数,然后又创建了一个基于 枚举A
的枚举。这样的定义是被允许的,在 enum
模块的规定下,一个新的 Enum
类必须基于一个 Enum
类,至多一个实体数据类型以及出于实际需要的任意多个基于 object
的 mixin
类。 这些基类的顺序为:
class EnumName([mix-in, ...,] [data-type,] base-enum): pass 复制代码
另外,没有定义枚举成员的枚举才能被其他枚举继承,否则将会报错:
class A(enum.Enum): AA = 1 class B(A): BB = 2 # TypeError: B: cannot extend enumeration 'A' 复制代码
第二部分
IntEnum
IntEnum
类是 Enum
类的子类,也是 int
的子类。 IntEnum
的枚举成员的值必须是 int
类型,填写其他类型会报错。
class IntTest(enum.IntEnum): A = 1 B = 'b' # ValueError: invalid literal for int() with base 10: 'b' 复制代码
除此之外, IntEnum
类和 Enum
类最大的区别在与比较时的差异,前文中并没有介绍 Enum
类比较功能的特性,这里先说一下 Enum
类的比较:
枚举成员只与自己本身和别名相等,枚举成员与非枚举值比较永远不相等,枚举成员只能无法进行排序比较。
class TestA(enum.Enum): A = 1 B = 1 class TestB(enum.Enum): A = 1 B = 2 print(TestA.A is TestB.A) # 不同枚举的同名同值的枚举成员的 is 比较 # False print(TestA.A is TestA.B) # 同一枚举的同值枚举成员 is 比较(别名) # True print(TestA.A == TestB.A) # 不同枚举的同名同值的枚举成员的 == 比较 # False print(TestA.A == TestA.B) # 同一枚举的同值枚举成员 == 比较(别名) print(TestA.A == 1) # 与 int 类型比较 # False print(TestA.A < TestA.B) # 排序比较 # Traceback (most recent call last): # File "e:\project\test\test.py", line 15, in <module> # print(TestA.A < TestA.B) # TypeError: '<' not supported between instances of 'TestA' and 'TestA' 复制代码
IntEnum
的枚举成员可以直接与 int
类型的值比较,也支持排序比较,并且不同枚举的同名同值的枚举成员使用 ==
比较时也相等。
class IntTestA(enum.IntEnum): A = 1 B = 1 class IntTestB(enum.IntEnum): A = 1 B = 2 print(IntTestA.A is IntTestB.A) # 不同枚举的同名同值的枚举成员的 is 比较 # False print(IntTestA.A is IntTestA.B) # 同一枚举的同值枚举成员 is 比较(别名) # True print(IntTestA.A == IntTestB.A) # 不同枚举的同名同值的枚举成员的 == 比较 # True print(IntTestA.A == IntTestA.B) # 同一枚举的同值枚举成员 == 比较(别名) # True print(IntTestA.A == 1) # 与 int 类型比较 # True print(IntTestA.A < IntTestB.B) # 排序比较 # True 复制代码
IntEnum
在其他方面的行为类似于 int
类型的数据:
class IntTestA(enum.IntEnum): A = 1 B = 2 print(['a', 'b', 'c'][IntTestA.A]) # 枚举成员作为索引 # b print([i for i in range(IntTestA.B)]) # 枚举成员作为 range() 函数的参数 # [0, 1] 复制代码
Flag
Flag
类是 Enum
类的子类, Flag
类与 Enum
类的差别就是 Flag
枚举成员可以使用按位运算符( &
、 |
、 ^
、 ~
)。
class TestFlag(enum.Flag): A = 1 B = 2 C = A | B print(list(TestFlag)) # 所有枚举成员 # [<TestFlag.A: 1>, <TestFlag.B: 2>, <TestFlag.C: 3>] print(repr(TestFlag.A | TestFlag.B)) # 枚举成员A 与 B 按位或 # <TestFlag.C: 3> print(repr(TestFlag.C)) # 枚举成员 C # <TestFlag.C: 3> print(1 | 2) # 1 与 2 按位或 # 3 复制代码
Flag
类的枚举成员可以在定义时使用按位运算符,也可以在使用枚举成员时使用按位运算符, 并且如果得到的值与枚举中的枚举成员的值相等时,最后得到的结果就是值与结果相同的枚举成员。那么如果在使用枚举成员时使用按位运算符,最后的结果在枚举中没有枚举成员的值与之相等呢?
class TestFlag(enum.Flag): A = 1 B = 2 print(repr(TestFlag.A | TestFlag.B)) # 枚举成员A 与 B 按位或 # <TestFlag.B|A: 3> print(type(TestFlag.A | TestFlag.B)) # 查看结果的类型 # <enum 'TestFlag'> 复制代码
Flag
类的枚举成员之间使用按位运算符之后, 如果枚举中有相同值的枚举成员时得到那个枚举成员,否则将获取一个新的枚举成员。
IntFlag
IntFlag
类是 Flag
与 Int
类的子类。其实, IntFlag
类更像是可以使用按位运算符的 IntEnum
。但在某些方面 IntFlag
还是有自己独有的特点。
首先, IntFlag
的枚举成员的值只能是数字,这点与 IntEnum
类相同。 IntFlag
的枚举成员在定义时可以使用按位运算符,在使用枚举成员时也可以使用按位运算符, 得到的结果还是枚举成员,这点与 Flag
类相同。
class TestIntFlag(enum.IntFlag): A = 1 B = 2 C = A | B print(repr(TestIntFlag.A | TestIntFlag.B)) # <TestIntFlag.C: 3> 复制代码
因为继承了 Int
类, IntFlag
类的枚举成员也可以像 IntEnum
类的枚举成员那样进行运算,作索引值等等。但是, IntFlag
类的枚举成员进行除按位运算以外的其他运算都将导致失去 IntFlag
类的枚举成员的资格。
class TestIntFlag(enum.IntFlag): A = 1 B = 2 print(TestIntFlag.A + TestIntFlag.B) # 加法运算 # 3 print(['a', 'b', 'c', 'd'][TestIntFlag.A]) # 索引值 # b 复制代码
自定义类
当以上枚举类都无法满足需求时,可以修改枚举类中的魔法方法来改变某些特性或功能。
__new__()
魔法方法在每个枚举成员定义前调用,可以修改枚举成员的值和设置自定义属性。
'''修改枚举成员定义的值''' class Test(enum.Enum): A = 'aaa' B = 'bbb' def __new__(cls, value): obj = object.__new__(cls) obj._value_ = str(value) + '...' return obj print(list(Test)) # [<Test.A: 'aaa...'>, <Test.B: 'bbb...'>] 复制代码
__init__()
魔法方法在枚举成员定义后调用,可实现的功能与 __new__()
魔法方法基本一致。这里官方文档中标明出了一点,虽然 __init__()
魔法方法可以定义枚举成员的值,但是不允许使用 __init__()
魔法方法定义枚举成员的值。
'''被官方文档禁止的操作, 修改枚举成员的值''' class Test(enum.Enum): A = 'aaa' B = 'bbb' def __init__(self, pantone='unknown'): self._value_ = str(self.value) + '...' print(list(Test)) # [<Test.A: 'aaa...'>, <Test.B: 'bbb...'>] 复制代码
__repr__()
魔法方法的重定义,用来修改使用 repr()
函数时返回的内容:
'''使用 repr() 函数时, 不展示枚举成员的值''' class TestA(enum.Enum): A = enum.auto() B = enum.auto() def __repr__(self): return '<%s.%s>' % (self.__class__.__name__, self.name) print(repr(TestA.A)) # <TestA.A> print(TestA.A.value) # 正常获取枚举成员的值 # 1 复制代码
同 __repr__()
魔法方法类似, __str__()
、 __add__()
、 __format__()
、 __bool__()
等等魔法方法都是可以重新定义的。
下面是官方文档中的两个有趣的小例子:
'''一个有序枚举,它不是基于 IntEnum,因此保持了正常的 Enum 不变特性''' class OrderedEnum(enum.Enum): def __ge__(self, other): if self.__class__ is other.__class__: return self.value >= other.value return NotImplemented def __gt__(self, other): if self.__class__ is other.__class__: return self.value > other.value return NotImplemented def __le__(self, other): if self.__class__ is other.__class__: return self.value <= other.value return NotImplemented def __lt__(self, other): if self.__class__ is other.__class__: return self.value < other.value return NotImplemented class Grade(OrderedEnum): A = 5 B = 4 C = 3 D = 2 F = 1 print(Grade.C < Grade.A) # True '''如果发现重复的成员名称则将引发错误而不是创建别名''' class DuplicateFreeEnum(enum.Enum): def __init__(self, *args): cls = self.__class__ if any(self.value == e.value for e in cls): a = self.name e = cls(self.value).name raise ValueError( "aliases not allowed in DuplicateFreeEnum: %r --> %r" % (a, e)) class Color(DuplicateFreeEnum): RED = 1 GREEN = 2 BLUE = 3 GRENE = 2 # ValueError: aliases not allowed in DuplicateFreeEnum: 'GRENE' --> 'GREEN' 复制代码
第三部分
不重要的枚举成员的值
在某些场景中,人们并不关心枚举成员的值是什么,可以使用下列几种方法定义枚举:
enum.auto() object() __new__()
看完以上的列举,这种实现这种需求并不局限于上面几种方法,只要最终枚举成员的值不重复即可,比如自定义一个随机生成字符串的函数。
import enum import pickle # 封存 '''enum.auto()''' class TestA(enum.Enum): A = enum.auto() B = enum.auto() '''object()''' class TestB(enum.Enum): A = object() B = object() '''描述字符串''' class TestC(enum.Enum): A = '字母A' B = '字母B' '''元组, 并自定义 __new__()''' class TestD(enum.Enum): A = () B = () def __new__(cls, *args): '''获取当前枚举成员数并加一, 将结果赋值给新定义的枚举成员''' value = len(cls.__members__) + 1 obj = object.__new__(cls) # 必须创建一个枚举成员对象 obj._value_ = value return obj # 必须返回一个枚举成员对象 print(list(TestA)) # [<TestA.A: 1>, <TestA.B: 2>] print(list(TestB)) # [<TestB.A: <object object at 0x0000023986178210>>, <TestB.B: <object object at 0x0000023986178220>>] print(list(TestC)) # [<TestC.A: '字母A'>, <TestC.B: '字母B'>] print(list(TestD)) # [<TestD.A: 1>, <TestD.B: 2>] 复制代码
封存
枚举可以被封存和解封:
class Test(enum.Enum): A = enum.auto() B = enum.auto() 封存的枚举成员A = pickle.dumps(Test.A) 解封的枚举成员A = pickle.loads(封存的枚举成员A) print(封存的枚举成员A) # b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94K\x01\x85\x94R\x94.' print(解封的枚举成员A) # Test.A print(Test.A is 解封的枚举成员A) # 解封后的数据与原数据是否一致 # True 复制代码
功能性API
Enum
类属于可调用对象,也就是说 Enum
类可以像函数一样被调用:
Enum(value='NewEnumName', names=<...>, *, module='...', qualname='...', type=<mixed-in class>, start=1) 参数: value: str, 枚举的名称 names: 枚举成员们的名称, 可使用多种方式设置枚举成员 module: 关键字参数, str 或 None, 指明所在的模块 qualname: 关键字参数, str, 指明所在的模块中的具体位置 type: 关键字参数, 指明枚举类的类型 start: 关键字参数, int, 枚举成员的起始值, 默认为 1 返回值: 枚举 复制代码
names
参数可以传入一个以空格或逗号隔开的字符串,分隔的子字符串就是枚举成员的名称,各枚举成员的值按顺序自增,起始值由 start
参数设置,起始值默认为 1
。 names
参数也可以传入迭代器或映射。
'''以空格分隔''' TestA = enum.Enum('TestEnum', 'A B C D') print(list(TestA)) # [<TestEnum.A: 1>, <TestEnum.B: 2>, <TestEnum.C: 3>, <TestEnum.D: 4>] '''以空格分隔, 设置起始值为10''' TestB = enum.Enum('TestEnum', 'A B C D', start=10) print(list(TestB)) # [<TestEnum.A: 10>, <TestEnum.B: 11>, <TestEnum.C: 12>, <TestEnum.D: 13>] '''传入列表设置枚举成员, 枚举成员的值按顺序自增, 起始值由 start 参数设置''' TestC = enum.Enum('TestEnum', ['A', 'B', 'C', 'D']) print(list(TestC)) # [<TestEnum.A: 1>, <TestEnum.B: 2>, <TestEnum.C: 3>, <TestEnum.D: 4>] '''传入列表, 并且元素格式为 ('名称', '值')''' TestD = enum.Enum('TestEnum', [('A', 11), ('B', 22), ('C', 33), ('D', 44)]) print(list(TestD)) # [<TestEnum.A: 11>, <TestEnum.B: 22>, <TestEnum.C: 33>, <TestEnum.D: 44>] '''传入映射, 格式为 {名称: 值}''' TestE = enum.Enum('TestEnum', {'A': 11, 'B': 22, 'C': 33, 'D': 44}) print(list(TestE)) # [<TestEnum.A: 11>, <TestEnum.B: 22>, <TestEnum.C: 33>, <TestEnum.D: 44>] 复制代码
module
参数需要传入一个模块,为了在进行封存操作时成功执行。若指定一个错误的模块,那么封存操作将报错。若参数值若设置为 None
或 __name__
最终指向的模块均为当前文件 __main__
。
'''module 参数默认为 None''' TestA = enum.Enum('TestA', 'AAA BBB CCC') print(pickle.loads(pickle.dumps(TestA))) # 封存+解封 # <enum 'TestA'> '''module 参数设置为 __name__''' TestB = enum.Enum('TestB', 'AAA BBB CCC', module=__name__) print(pickle.loads(pickle.dumps(TestB))) # <enum 'TestB'> '''module 参数设置为一个不存在的模块''' TestD = enum.Enum('TestD', 'AAA BBB CCC', module='aaa') print(pickle.loads(pickle.dumps(TestD))) # _pickle.PicklingError: Can't pickle <enum 'TestD'>: import of module 'aaa' failed 复制代码
qualname
参数是指明在模块中的具体位置,同 module
参数类似,设置这个参数的目的也是为了封存操作, pickle
协议版本 4
在某些情况下需要依赖设置的具体位置。
布尔值
枚举类的布尔值总是 True
,枚举中的枚举成员的布尔值也总是 True
。 枚举成员的布尔值与枚举成员的值无关,如果想要以枚举成员的值来计算布尔值,可以修改 __bool__()
魔法方法。
'''不修改 __bool__() 魔法方法的枚举类''' class Test(enum.Enum): A = 0 B = None print(bool(Test)) # True print(bool(Test.A)) # True print(bool(Test.B)) # True '''修改 __bool__() 魔法方法的枚举类''' class Test(enum.Enum): A = 0 print(bool(Test.A)) # False 复制代码
支持枚举类使用的属性
__members__
是一个 member_name:member
条目的只读有序映射。得到的结果是以 {名称: 枚举成员对象}
为结构的字典。
class Test(enum.Enum): A = 1 B = 2 C = 1 print(Test.__members__) # {'A': <Test.A: 1>, 'B': <Test.B: 2>, 'C': <Test.A: 1>} 复制代码
支持枚举成员使用的属性
-
_name_
和_value_
分别返回枚举成员的名称和值; -
_generate_next_value_
在功能性API中被使用,为枚举成员获取适当的值,也可被打印; -
_missing_
作用是当未发现某个值时所使用的查找函数; -
_ignore_
是一个名称列表,结果可以是list
,也可以是str
,不会转化为枚举成员,并将从最终类中被移除; -
_order_
是Python2
的遗留属性,在Python2
中负责记录枚举成员的定义顺序;
'''可直接打印的属性''' class Test(enum.Enum): A = 1 B = 2 C = 1 print(Test.A._name_) # 'A' print(Test.A._value_) # 1 print(Test.A._generate_next_value_) # <bound method Enum._generate_next_value_ of <Test.A: 1>> '''_ignore_的用法''' class Period(enum.Enum): _ignore_ = 'Period i' Period = vars() for i in range(3): Period['obj_%d' % i] = i print(list(Period)) # [<Period.obj_0: 0>, <Period.obj_1: 1>, <Period.obj_2: 2>] 复制代码
参考资料
官方文档: docs.python.org/zh-cn/3/lib…
源代码: Lib/enum.py