枚举(enum)介绍

引入

包括 JavaC++ 在内的几种编程语言在其语法中具有本机枚举或枚举数据类型。此数据类型允许您创建命名常量集,这些常量被视为包含枚举的成员。可以通过枚举本身访问成员。

当您需要定义一组不可变离散的相似或相关常量值时,枚举会派上用场,这些常量值在代码中可能具有也可能没有语义含义。

一周中的几天、一年中的月份和季节、地球的基本方向、程序的状态代码、HTTP 状态代码、交通信号灯中的颜色以及 Web 服务的定价计划都是编程中枚举的很好的例子。通常,只要您有一个变量可以采用一组有限的可能值之一,就可以使用枚举。

Python 没有枚举数据类型作为其语法的一部分。幸运的是,Python 3.4 将枚举模块添加到标准库中。此模块提供了 Enum 类,用于支持 Python 中的通用枚举。

PEP 435 引入了枚举,其定义如下:

枚举是绑定到唯一常量值的一组符号名称。在枚举中,可以按标识比较值,并且可以迭代枚举本身。

在向标准库添加此内容之前,可以通过定义一系列相似或相关的常量来创建类似于枚举的内容。

RED,GREEN,YELLOW=range(3)

尽管这个习惯用语有效,但当您尝试对大量相关常量进行分组时,它不能很好地扩展。另一个不便是第一个常量的值为 ,这在 Python 中是伪造的。在某些情况下,这可能是一个问题,尤其是涉及布尔测试的情况

在大多数情况下,枚举可以帮助您避免上述习惯用法的缺点。它们还将帮助您生成更有条理、可读性和健壮的代码。枚举有几个好处,其中一些与编码的易用性有关:

  • 允许方便地将相关常量分组到某种命名空间
  • 允许使用对枚举成员或枚举本身进行操作的自定义方法的其他行为
  • 提供对枚举成员的快速灵活访问
  • 启用对成员的直接迭代,包括其名称和值
  • 促进 IDE 和编辑器中的代码完成
  • 使用静态检查器启用类型和****错误检查
  • 提供可搜索名称的中心
  • 减少使用枚举成员时的拼写错误

它们还通过提供以下好处使您的代码健壮:

  • 确保在代码执行期间无法更改的常量值
  • 通过区分多个枚举之间共享的相同值来保证类型安全
  • 通过使用描述性名称而不是神秘值或幻数来提高可读性和可****维护性
  • 通过利用可读名称而不是没有明确含义的值来促进调试
  • 在整个代码中提供单一事实来源一致性

使用Python创建枚举Enum

Python 的模块提供了类,它允许你创建枚举类型。若要创建自己的枚举,可以子类或使用其函数 enumAPI。这两个选项都允许您将一组相关常量定义为枚举成员。

通过子类创建枚举Enum

该模块定义具有迭代比较功能的通用枚举类型。可以使用此类型创建可用于替换常见数据类型(如数字和字符串)的文本的命名常量集。

何时应使用枚举的一个典型示例是,当您需要创建一组表示星期几的枚举常量时。每一天都有一个符号名称和一个介于 1和 (包括 和 )7之间的数值。

from enum import Enum


class Day(Enum):
    MONDAY, THUSDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(1, 8)


print(list(Day))
"""
[<Day.MONDAY: 1>, <Day.THUSDAY: 2>, <Day.WEDNESDAY: 3>, <Day.THURSDAY: 4>, <Day.FRIDAY: 5>, 
<Day.SATURDAY: 6>, <Day.SUNDAY: 7>]

"""
print(Day.FRIDAY)
print(Day)

DayEnum的子类。因此,你可以调用整个枚举集合也可以只调用枚举成员。

由于枚举成员必须是常量,因此 Python 不允许你在运行时为枚举成员分配新值:

Day.FRIDAY=0
"""
AttributeError: Cannot reassign members.
"""

如果尝试更改枚举成员的值,则会得到一个 .与成员名称不同,包含枚举本身的名称不是常量,而是变量。因此,可以在程序执行期间随时重新绑定此名称,但应避免这样做。

由于枚举用于表示常量,因此我们建议对枚举成员使用UPPER_CASE名称...

可以将枚举视为常量的集合。与列表、元组字典一样,Python 枚举也是可迭代的。这就是为什么你可以使用 list() 将枚举转换为枚举成员的原因。

即使使用class语法创建枚举,它们也是与普通 Python 类不同的特殊类。与常规类不同,枚举:

当你开始在 Python 中创建和使用自己的枚举时,你应该记住所有这些细微的差异。

通常,枚举的成员采用连续的整数值。但是,在 Python 中,成员的值可以是任何类型的,包括用户定义的类型。例如,下面是按降序使用非连续数值的学校成绩枚举:

>>> from enum import Enum

>>> class Grade(Enum):
...     A = 90
...     B = 80
...     C = 70
...     D = 60
...     F = 0
...

>>> list(Grade)
[
    <Grade.A: 90>,
    <Grade.B: 80>,
    <Grade.C: 70>,
    <Grade.D: 60>,
    <Grade.F: 0>
]

这个例子表明 Python 枚举非常灵活,允许你对其成员使用任何有意义的值。

使用异类值定义枚举

>>> from enum import Enum

>>> class UserResponse(Enum):
...     YES = 1
...     NO = "No"
...

>>> UserResponse.NO
<UserResponse.NO: 'No'>

>>> UserResponse.YES
<UserResponse.YES: 1>

但是,从类型安全的角度来看,这种做法会使代码不一致。因此,不建议这样做。理想情况下,如果您具有相同数据类型的值,这将有所帮助,这与在枚举中对相似的相关常量进行分组的想法一致。

空枚举

>>> from enum import Enum

>>> class Empty(Enum):
...     pass
...

>>> list(Empty)
[]

>>> class Empty(Enum):
...     ...
...

>>> list(Empty)
[]

>>> class Empty(Enum):
...     """Empty enumeration for such and such purposes."""
...

>>> list(Empty)
[]

在此示例中, 表示空枚举,因为它未定义任何成员常量。请注意,可以使用 pass 语句、省略号文本 () 或类级文档字符串来创建空枚举。最后一种方法可以通过在文档字符串中提供额外的上下文来帮助您提高代码的可读性

现在,为什么还需要定义一个空枚举?当您需要构建枚举类的层次结构以通过继承重用功能时,空枚举会派上用场。

>>> from enum import Enum
>>> import string

>>> class BaseTextEnum(Enum):
...     def as_list(self):
...         try:
...             return list(self.value)
...         except TypeError:
...             return [str(self.value)]
...

>>> class Alphabet(BaseTextEnum):
...     LOWERCASE = string.ascii_lowercase
...     UPPERCASE = string.ascii_uppercase
...

>>> Alphabet.LOWERCASE.as_list()
['a', 'b', 'c', 'd', ..., 'x', 'y', 'z']

在此示例中,创建为没有成员的枚举。仅当自定义枚举没有成员时,才能对其进行子类化,因此符合条件。该类继承自空枚举,这意味着您可以访问该方法。此方法将给定成员的值转换为列表

使用函数式API创建枚举

该类提供了一个功能性 API,Enum可用于创建枚举,而无需使用通常的类语法。你只需要使用适当的参数进行调用,就像使用函数或任何其他可调用对象一样.

此函数式 API 类似于 namedtuple() 工厂函数的工作方式。在Enum 的情况下,功能签名具有以下形式:

Enum(
    value,
    names,
    *,
    module=None,
    qualname=None,
    type=None,
    start=1
)

image-20230203173230753

示例:

>>> from enum import Enum

>>> HTTPMethod = Enum(
...     "HTTPMethod", ["GET", "POST", "PUSH", "PATCH", "DELETE"]
... )

>>> list(HTTPMethod)
[
    <HTTPMethod.GET: 1>,
    <HTTPMethod.POST: 2>,
    <HTTPMethod.PUSH: 3>,
    <HTTPMethod.PATCH: 4>,
    <HTTPMethod.DELETE: 5>
]

此调用 返回一个名为 的新枚举。若要提供成员名称,请使用字符串列表。每个字符串表示一个 HTTP 方法。请注意,成员值会自动设置为从 开始的连续整数。您可以使用参数更改此初始值。

使用类语法或函数 API 创建枚举是您的决定,主要取决于您的品味和具体条件。但是,如果要动态创建枚举,则函数式 API 可能是唯一的选择。

最后,如果需要为枚举成员设置自定义值,则可以使用名称-值对的可迭代对象作为参数。在下面的示例中,使用名称-值元组列表来初始化所有枚举成员:

>>> from enum import Enum

>>> HTTPStatusCode = Enum(
...     value="HTTPStatusCode",
...     names=[
...         ("OK", 200),
...         ("CREATED", 201),
...         ("BAD_REQUEST", 400),
...         ("NOT_FOUND", 404),
...         ("SERVER_ERROR", 500),
...     ],
... )

>>> list(HTTPStatusCode)
[
    <HTTPStatusCode.OK: 200>,
    <HTTPStatusCode.CREATED: 201>,
    <HTTPStatusCode.BAD_REQUEST: 400>,
    <HTTPStatusCode.NOT_FOUND: 404>,
    <HTTPStatusCode.SERVER_ERROR: 500>
]

提供名称-值元组列表(如上所示),可以使用成员的自定义值创建枚举。在此示例中,如果不想使用名称-值元组列表,则还可以使用将名称映射到值的字典

HTTPStatusCode = Enum(
        value="HTTPStatusCode",
        names=[
                ("OK", 200),
                ("CREATED", 201),
                ("BAD_REQUEST", 400),
                ("NOT_FOUND", 404),
                ("SERVER_ERROR", 500)
                ]
        )

print(list(HTTPStatusCode))
HTTPStatusCode2 = Enum(
        value="HTTPStatusCode",
        names=dict([
                ("OK", 200),
                ("CREATED", 201),
                ("BAD_REQUEST", 400),
                ("NOT_FOUND", 404),
                ("SERVER_ERROR", 500)
                ])
        )
print(list(HTTPStatusCode2))
"""
[<HTTPStatusCode.OK: 200>, <HTTPStatusCode.CREATED: 201>, <HTTPStatusCode.BAD_REQUEST: 400>, 
<HTTPStatusCode.NOT_FOUND: 404>, <HTTPStatusCode.SERVER_ERROR: 500>]
[<HTTPStatusCode.OK: 200>, <HTTPStatusCode.CREATED: 201>, <HTTPStatusCode.BAD_REQUEST: 400>, 
<HTTPStatusCode.NOT_FOUND: 404>, <HTTPStatusCode.SERVER_ERROR: 500>]

"""

从自动值生成枚举

Python 的enum模块提供了一个名为 auto() 的方便函数,它允许您为枚举成员设置自动值。此函数的默认行为是将连续的整数值分配给成员。

# 自动生成
from enum import auto, Enum


class Day(Enum):
    MONDAY = auto()
    TUESDAY = auto()
    WEDNESDAY = 4
    THURSDAY = auto()
    FRIDAY = auto()
    SUNDAY = 7


print(list(Day))
"""
[<Day.MONDAY: 1>, <Day.TUESDAY: 2>, <Day.WEDNESDAY: 4>, <Day.THURSDAY: 5>, <Day.FRIDAY: 6>, 
<Day.SUNDAY: 7>]

"""

缺省情况下, 将连续的整数分配给从 开始的每个目标成员。

您可以通过重写 ._generate_next_value_() 方法来调整此默认行为,该方法在后台使用自动生成值。

# 您可以通过重写 ._generate_next_value_() 方法来调整此默认行为,该方法在后台使用自动生成值。
class CardinalDirection(Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name[0]

    NORTH = auto()
    SOUTH = auto()
    EAST = auto()
    WEST = auto()


print(list(CardinalDirection))
"""
[<CardinalDirection.NORTH: 'N'>, <CardinalDirection.SOUTH: 'S'>, <CardinalDirection.EAST: 'E'>, 
<CardinalDirection.WEST: 'W'>]


"""

创建具有别名和唯一值的枚举

可以创建两个或多个成员具有相同常量值的枚举。冗余成员称为别名,在某些情况下可能很有用。例如,假设您有一个包含一组操作系统 (OS) 的枚举,如以下代码所示:

>>> from enum import Enum

>>> class OperatingSystem(Enum):
...     UBUNTU = "linux"
...     MACOS = "darwin"
...     WINDOWS = "win"
...     DEBIAN = "linux"
...

>>> # Aliases aren't listed
>>> list(OperatingSystem)
[
    <OperatingSystem.UBUNTU: 'linux'>,
    <OperatingSystem.MACOS: 'darwin'>,
    <OperatingSystem.WINDOWS: 'win'>
]

>>> # To access aliases, use __members__
>>> list(OperatingSystem.__members__.items())
[
    ('UBUNTU', <OperatingSystem.UBUNTU: 'linux'>),
    ('MACOS', <OperatingSystem.MACOS: 'darwin'>),
    ('WINDOWS', <OperatingSystem.WINDOWS: 'win'>),
    ('DEBIAN', <OperatingSystem.UBUNTU: 'linux'>)
]

Linux 发行版被视为独立的操作系统。因此,Ubuntu 和 Debian 都是独立的系统,有着不同的目标和目标受众。但是,它们共享一个名为Linux的公共内核

上面的枚举将操作系统映射到其相应的内核。此关系将转换为 的别名,当您拥有与内核相关的代码以及特定于给定 Linux 发行版的代码时,这可能很有用。DEBIAN``UBUNTU

在上面的示例中需要注意的一个重要行为是,当您直接循环访问枚举时,不会考虑别名。如果需要遍历所有成员(包括别名),则需要使用 .你将在有关循环访问枚举的部分中了解有关迭代和属性的详细信息。.__members__``.__members__

还可以选择在枚举中完全禁止别名。为此,您可以使用模块中的@unique装饰器enum

>>> from enum import Enum, unique

>>> @unique
... class OperatingSystem(Enum):
...     UBUNTU = "linux"
...     MACOS = "darwin"
...     WINDOWS = "win"
...     DEBIAN = "linux"
...
Traceback (most recent call last):
    ...
ValueError: duplicate values in <enum 'OperatingSystem'>: DEBIAN -> UBUNTU

在此示例中OperatingSystem,您使用@unique 进行装饰。如果任何成员值重复,则得到一个 ValueError.在这里,异常消息指出DEBIAN和UBUNTU并共享相同的值,这是不允许的。

在python中使用枚举

访问枚举成员

在代码中使用枚举时,访问其成员是要执行的基本操作。您将有三种不同的方法来访问 Python 中的枚举成员。

例如,假设您需要访问下面的枚举成员NORTH。在这种情况下,您可以执行以下操作:CardinalDirection

>>> from enum import Enum

>>> class CardinalDirection(Enum):
...     NORTH = "N"
...     SOUTH = "S"
...     EAST = "E"
...     WEST = "W"
...

>>> # Dot notation
>>> CardinalDirection.NORTH
<CardinalDirection.NORTH: 'N'>

>>> # Call notation
>>> CardinalDirection("N")
<CardinalDirection.NORTH: 'N'>

>>> # Subscript notation
>>> CardinalDirection["NORTH"]
<CardinalDirection.NORTH: 'N'>
  • 使用.点表示法访问枚举成员
  • 使用成员的值作为参数调用枚举来访问目标成员

注意:请务必注意,使用成员的值作为参数调用枚举会让您感觉自己正在实例化枚举。但是,枚举无法实例化,如您所知:

>>> week = Day()
Traceback (most recent call last):
    ...
TypeError: EnumMeta.__call__() missing 1 required positional argument: 'value'

不允许尝试创建现有枚举的实例,因此,如果尝试这样做,则会得到 TypeError。因此,不得将实例化与通过枚举调用访问成员混淆。

  • 使用类似字段的表示法或者下标表示法来访问使用成员名称作为目标键的成员

使用.name.value属性

Python枚举的成员是器包含类的实例。在枚举类分析期间,键自动为每个成员提供一个属性.name,该属性将成员的名称保存为字符串。成员还获取一个属性,该属性将分配给成员本身的值存储在类定义中.value.

>>> from enum import Enum

>>> class Semaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...

>>> Semaphore.RED.name
'RED'

>>> Semaphore.RED.value
1

>>> Semaphore.YELLOW.name
'YELLOW'

循环访问枚举

与常规类相比,Python枚举类的一个显著特征是枚举在默认情况下是可迭代的。

Python的枚举支持按定义顺序直接迭代成员。

>>> from enum import Enum

>>> class Flavor(Enum):
...     VANILLA = 1
...     CHOCOLATE = 2
...     MINT = 3
...

>>> for flavor in Flavor:
...     print(flavor)
...
Flavor.VANILLA
Flavor.CHOCOLATE
Flavor.MINT

访问属性:

>>> for flavor in Flavor:
...     print(flavor.name, "->", flavor.value)
...
VANILLA -> 1
CHOCOLATE -> 2
MINT -> 3

枚举具有称为__members__的特殊属性,也可以使用该属性循环访问器成员。此属性包含将名称映射到成员的字典。

迭代此字典和直接迭代枚举之间的区别在于,字典允许您访问 枚举的所有成员,包括您可能拥有的所有别名

>>> for name in Flavor.__members__:
...     print(name)
...
VANILLA
CHOCOLATE
MINT

>>> for name in Flavor.__members__.keys():
...     print(name)
...
VANILLA
CHOCOLATE
MINT

>>> for member in Flavor.__members__.values():
...     print(member)
...
Flavor.VANILLA
Flavor.CHOCOLATE
Flavor.MINT

>>> for name, member in Flavor.__members__.items():
...     print(name, "->", member)
...
VANILLA -> Flavor.VANILLA
CHOCOLATE -> Flavor.CHOCOLATE
MINT -> Flavor.MINT

ifmatch语句中使用枚举

枚举常用的场景:if ...elifmatch case(3.10);

>>> from enum import Enum

>>> class Semaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...

>>> def handle_semaphore(light):
...     if light is Semaphore.RED:
...         print("You must stop!")
...     elif light is Semaphore.YELLOW:
...         print("Light will change to red, be careful!")
...     elif light is Semaphore.GREEN:
...         print("You can continue!")
...

>>> handle_semaphore(Semaphore.GREEN)
You can continue!

>>> handle_semaphore(Semaphore.YELLOW)
Light will change to red, be careful!

>>> handle_semaphore(Semaphore.RED)
You must stop!
>>> from enum import Enum

>>> class Semaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...

>>> def handle_semaphore(light):
...     match light:
...         case Semaphore.RED:
...             print("You must stop!")
...         case Semaphore.YELLOW:
...             print("Light will change to red, be careful!")
...         case Semaphore.GREEN:
...             print("You can continue!")
...

>>> handle_semaphore(Semaphore.GREEN)
You can continue!

>>> handle_semaphore(Semaphore.YELLOW)
Light will change to red, be careful!

>>> handle_semaphore(Semaphore.RED)

比较枚举

默认情况下,枚举支持两种类型的比较运算符:

  • identity:using the is and is notoperators
  • Equality:using the ==and !=operators

Identity:比较依赖于每个枚举成员都是其枚举类的单一实例这一事实。

>>> from enum import Enum

>>> class AtlanticAveSemaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...     PEDESTRIAN_RED = 1
...     PEDESTRIAN_GREEN = 3
...

>>> red = AtlanticAveSemaphore.RED
>>> red is AtlanticAveSemaphore.RED
True
>>> red is not AtlanticAveSemaphore.RED
False

>>> yellow = AtlanticAveSemaphore.YELLOW
>>> yellow is red
False
>>> yellow is not red
True

>>> pedestrian_red = AtlanticAveSemaphore.PEDESTRIAN_RED
>>> red is pedestrian_red
True

每个枚举成员都有自己的身份,这与它的同级成员的身份不同。此规则不适用于成员别名,因为它们只是对现有成员的引用,并且共享相同的标识。这就是在最后一个示例中比较和返回的原因。

要在Python中获取给定对象的标识,可以使用内置的id()函数和该对象作为参数

不同枚举的成员之间的标识(identity)检查始终返回False

>>> class EighthAveSemaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...     PEDESTRIAN_RED = 1
...     PEDESTRIAN_GREEN = 3
...

>>> AtlanticAveSemaphore.RED is EighthAveSemaphore.RED
False

>>> AtlanticAveSemaphore.YELLOW is EighthAveSemaphore.YELLOW
False

False结果的原因是不同枚举的成员都具有单独的实例,因此对它们的任何id()检查都会返回False;

相等运算符(equality)

>>> from enum import Enum

>>> class AtlanticAveSemaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...     PEDESTRIAN_RED = 1
...     PEDESTRIAN_GREEN = 3
...

>>> red = AtlanticAveSemaphore.RED
>>> red == AtlanticAveSemaphore.RED
True

>>> red != AtlanticAveSemaphore.RED
False

>>> yellow = AtlanticAveSemaphore.YELLOW
>>> yellow == red
False
>>> yellow != red
True

>>> pedestrian_red = AtlanticAveSemaphore.PEDESTRIAN_RED
>>> red == pedestrian_red
True

枚举成员始终具有一个具体值,该值可以是数字、字符串或任何其他对象。因此,在枚举成员和公共对象之间运行相等比较可能很诱人。

但是,这种比较无法按预期工作,因为实际比较基于对象标识:

>>> from enum import Enum

>>> class Semaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...

>>> Semaphore.RED == 1
False

>>> Semaphore.YELLOW == 2
False

>>> Semaphore.GREEN != 3
True

即使成员值等于每个示例中的整数,这些比较也会返回False 。这是因为常规枚举成员按对象标识(identity)而不是按值进行比较。在上面的示例中,您将枚举成员与整数进行比较,这就像比较苹果和橙子一样。他们永远不会平等地比较,因为他们有不同的身份。

最后,枚举的另一个与比较相关的功能是,您可以使用 in ``和 not in 运算符对枚举执行成员资格测试:

>>> from enum import Enum

>>> class Semaphore(Enum):
...     RED = 1
...     YELLOW = 2
...     GREEN = 3
...

>>> Semaphore.RED in Semaphore
True

>>> Semaphore.GREEN not in Semaphore
False

默认情况下,Python 的枚举支持in andnot in 运算符。使用这些运算符,可以检查给定枚举中是否存在给定成员。

对枚举进行排序

默认情况下,Python的枚举不支持>、<,>=,<=等比较运算符。所以不能直接使用内置的sorted函数对枚举成员进行排序:

>>> from enum import Enum

>>> class Season(Enum):
...     SPRING = 1
...     SUMMER = 2
...     AUTUMN = 3
...     WINTER = 4
...

>>> sorted(Season)
Traceback (most recent call last):
    ...
TypeError: '<' not supported between instances of 'Season' and 'Season'

当你使用枚举作为sorted的参数时,您将会得到一个TypeError``<``key``sorted()。因为枚举不支持运算符,但是有一种方法可以使用调用中的参数按成员的名成和值成功对枚举进行排序。

>>> sorted(Season, key=lambda season: season.value)
[
    <Season.SPRING: 1>,
    <Season.SUMMER: 2>,
    <Season.AUTUMN: 3>,
    <Season.WINTER: 4>
]

>>> sorted(Season, key=lambda season: season.name)
[
    <Season.AUTUMN: 3>,
    <Season.SPRING: 1>,
    <Season.SUMMER: 2>,
    <Season.WINTER: 4>
]

扩展枚举

在前面的部分中,你已了解如何在 Python 代码中创建和使用枚举。到目前为止,你已使用默认枚举。这意味着您仅将 Python 的枚举与其标准功能和行为一起使用。

有时,可能需要为枚举提供自定义行为。为此,您可以向枚举添加方法并实现所需的功能。您也可以使用mixin类。在以下部分中,你将了解如何利用这两种技术来自定义枚举。

添加或调整成员方法

可以通过向枚举类添加新方法来为枚举提供新功能,就像处理任何常规python类一样。枚举时具有特殊功能的类。与常规类一样,枚举可以由方法和特殊方法。

>>> from enum import Enum

>>> class Mood(Enum):
...     FUNKY = 1
...     MAD = 2
...     HAPPY = 3
...
...     def describe_mood(self):
...         return self.name, self.value
...
...     def __str__(self):
...         return f"I feel {self.name}"
...
...     @classmethod
...     def favorite_mood(cls):
...         return cls.HAPPY
...

>>> Mood.HAPPY.describe_mood()
('HAPPY', 3)

>>> print(Mood.HAPPY)
I feel HAPPY

>>> Mood.favorite_mood()
<Mood.HAPPY: 3>

在此示例中,您有一个包含三个成员的枚举。常规方法(如Mood.describe_mood())绑定到其包含枚举的实例,这些枚举是枚举成员。因此,您必须在枚举成员上调用常规方法,而不是在枚举类本身上调用常规方法

请记住,Python的枚举无法实例化。枚举的成员是枚举的实例。因此,self表示当前成员。

__str__特殊方法对成员进行操作,为每个成员提供良好的可打印表示形式。

favorite_mood是一个类方法,它对类或枚举本身进行操作。像这样的类方法提供从类内部对所有枚举成员的访问。

当您需要实现策略模式时,您还可以利用此功能来包含其他行为。例如,假设您需要一个类,该类允许您使用两种策略按升序和降序对数字列表进行排序。在这种情况下,可以使用如下所示的枚举:

>>> from enum import Enum

>>> class Sort(Enum):
...     ASCENDING = 1
...     DESCENDING = 2
...     def __call__(self, values):
...         return sorted(values, reverse=self is Sort.DESCENDING)
...

>>> numbers = [5, 2, 7, 6, 3, 9, 8, 4]

>>> Sort.ASCENDING(numbers)
[2, 3, 4, 5, 6, 7, 8, 9]

>>> Sort.DESCENDING(numbers)
[9, 8, 7, 6, 5, 4, 3, 2]

将枚举与其它类型混合使用

Python支持多重继承作为其面向对象功能的一部分,这意味着在Python中,您可以在创建类层次结构是继承多个类。当您想要同时重用多个类的功能时,多重继承会派上用场。

面向对象编程中的一种常见做法是使用所谓的 mixin 类。这些类提供其他类可以使用的功能。在 Python 中,您可以将 mixin 类添加到给定类的父类列表中,以自动获取 mixin 功能。

例如,假设您需要一个支持整数比较的枚举。在这种情况下,您可以在定义枚举时使用内置类型作为 mixin:int

>>> from enum import Enum

>>> class Size(int, Enum):
...     S = 1
...     M = 2
...     L = 3
...     XL = 4
...

>>> Size.S > Size.M
False
>>> Size.S < Size.M
True
>>> Size.L >= Size.M
True
>>> Size.L <= Size.M
False

>>> Size.L > 2
True
>>> Size.M < 1
False

探索其它枚举类

生成整数枚举IntEnum

>>> from enum import IntEnum

>>> class Size(IntEnum):
...     S = 1
...     M = 2
...     L = 3
...     XL = 4
...

>>> Size.S > Size.M
False
>>> Size.S < Size.M
True
>>> Size.L >= Size.M
True
>>> Size.L <= Size.M
False

>>> Size.L > 2
True
>>> Size.M < 1
False

创建整数标志IntFalgFlag

可以使用 IntFlag 作为应支持按位运算符的枚举的基类。对子类的成员执行按位运算将返回一个对象,该对象也是基础枚举的成员。

>>> from enum import IntFlag

>>> class Role(IntFlag):
...     OWNER = 8
...     POWER_USER = 4
...     USER = 2
...     SUPERVISOR = 1
...     ADMIN = OWNER | POWER_USER | USER | SUPERVISOR
...

>>> john_roles = Role.USER | Role.SUPERVISOR
>>> john_roles
<Role.USER|SUPERVISOR: 3>

>>> type(john_roles)
<enum 'Role'>

>>> if Role.USER in john_roles:
...     print("John, you're a user")
...
John, you're a user

>>> if Role.SUPERVISOR in john_roles:
...     print("John, you're a supervisor")
...
John, you're a supervisor

>>> Role.OWNER in Role.ADMIN
True

>>> Role.SUPERVISOR in Role.ADMIN
True

使用枚举:两个实例

Python 的枚举可以帮助您提高代码的可读性和组织性。可以使用它们对类似的常量进行分组,然后可以在代码中使用这些常量将字符串、数字和其他值替换为可读且有意义的名称。

在以下部分中,您将编写几个处理常见枚举用例的实际示例。这些示例将帮助您确定代码何时可以从使用枚举中受益。

替代幻数

当您需要替换相关幻数集(如 HTTP 状态代码、计算机端口和退出代码)时,枚举非常有用。通过枚举,可以对这些数值常量进行分组,并为其分配可读和描述性名称,以便以后在代码中使用和重用这些名称。

假设您具有以下函数作为直接从 Web 检索和处理 HTTP 内容的应用程序的一部分

>>> from http.client import HTTPSConnection

>>> def process_response(response):
...     match response.getcode():
...         case 200:
...             print("Success!")
...         case 201:
...             print("Successfully created!")
...         case 400:
...             print("Bad request")
...         case 404:
...             print("Not Found")
...         case 500:
...             print("Internal server error")
...         case _:
...             print("Unexpected status")
...

>>> connection = HTTPSConnection("www.python.org")
>>> try:
...     connection.request("GET", "/")
...     response = connection.getresponse()
...     process_response(response)
... finally:
...     connection.close()
...
Success!

尽管此代码有效,但对于不熟悉 HTTP 状态代码及其相应含义的人来说,阅读和理解可能具有挑战性。若要解决这些问题并使代码更具可读性和可维护性,可以使用枚举对 HTTP 状态代码进行分组并为其指定描述性名称:

>>> from enum import IntEnum
>>> from http.client import HTTPSConnection

>>> class HTTPStatusCode(IntEnum):
...     OK = 200
...     CREATED = 201
...     BAD_REQUEST = 400
...     NOT_FOUND = 404
...     SERVER_ERROR = 500
...

>>> def process_response(response):
...     match response.getcode():
...         case HTTPStatusCode.OK:
...             print("Success!")
...         case HTTPStatusCode.CREATED:
...             print("Successfully created!")
...         case HTTPStatusCode.BAD_REQUEST:
...             print("Bad request")
...         case HTTPStatusCode.NOT_FOUND:
...             print("Not Found")
...         case HTTPStatusCode.SERVER_ERROR:
...             print("Internal server error")
...         case _:
...             print("Unexpected status")
...

>>> connection = HTTPSConnection("www.python.org")
>>> try:
...     connection.request("GET", "/")
...     response = connection.getresponse()
...     process_response(response)
... finally:
...     connection.close()
...
Success!

此代码向应用程序添加一个调用的新枚举。此枚举对目标 HTTP 状态代码进行分组,并为其提供可读的名称HTTPStatusCode。它还使它们严格恒定,从而使您的应用程序更加可靠。

process_response() 中,您可以使用人类可读的描述性名称来提供上下文和内容信息。现在,任何阅读您的代码的人都会立即知道匹配标准是 HTTP 状态代码。他们还会快速发现每个目标代码的含义。

创建状态机

枚举的另一个有趣用例是当您使用它们来重新创建给定系统的不同可能状态时。如果您的系统在任何给定时间都可以处于有限数量的状态之一,那么您的系统将作为状态机工作。当您需要实现此通用设计模式时,枚举非常有用。

作为如何使用枚举实现状态机模式的示例,您将创建一个最小的磁盘播放器模拟器。首先,继续并创建一个包含以下内容的文件:disk_player.py

# disk_player.py

from enum import Enum, auto

class State(Enum):
    EMPTY = auto()
    STOPPED = auto()
    PAUSED = auto()
    PLAYING = auto()

    
# disk_player.py
# ...

class DiskPlayer:
    def __init__(self):
        self.state = State.EMPTY

    def insert_disk(self):
        if self.state is State.EMPTY:
            self.state = State.STOPPED
        else:
            raise ValueError("disk already inserted")

    def eject_disk(self):
        if self.state is State.EMPTY:
            raise ValueError("no disk inserted")
        else:
            self.state = State.EMPTY

    def play(self):
        if self.state in {State.STOPPED, State.PAUSED}:
            self.state = State.PLAYING

    def pause(self):
        if self.state is State.PLAYING:
            self.state = State.PAUSED
        else:
            raise ValueError("can't pause when not playing")

    def stop(self):
        if self.state in {State.PLAYING, State.PAUSED}:
            self.state = State.STOPPED
        else:
            raise ValueError("can't stop when not playing or paused")
            
# disk_player.py
# ...

if __name__ == "__main__":
    actions = [
        DiskPlayer.insert_disk,
        DiskPlayer.play,
        DiskPlayer.pause,
        DiskPlayer.stop,
        DiskPlayer.eject_disk,
        DiskPlayer.insert_disk,
        DiskPlayer.play,
        DiskPlayer.stop,
        DiskPlayer.eject_disk,
    ]
    player = DiskPlayer()
    for action in actions:
        action(player)
        print(player.state)

结论

您现在知道如何在 Python 中创建和使用枚举。枚举(或只是枚举)是许多编程语言中常见且流行的数据类型。使用枚举,可以对相关常量集进行分组,并通过枚举本身访问它们。

Python 不提供专用的枚举语法。但是,enum.Enum该模块通过类支持此通用数据类型.


参考:使用 Python 的枚举构建常量枚举 – 真正的 Python (realpython.com)

posted @ 2023-04-11 09:36  LgRun  阅读(460)  评论(0编辑  收藏  举报