我的最佳实践 - Python Enum类的常用场景

Nov. 14, 2018, 2:34 a.m.

经常会和朋友分享Enum的用法,可能是口述的原因吧,每次交流都不是很充分和全面,干脆写下来吧。

先举个例子

  • 要描述的对象分为3部分,还要明确一点,他们存在从属关系,状态属于桌子,状态的值属于状态
  • 桌子
    • 桌子的状态
    • 状态的值(等待中、进行中、已结束)
  • 分解完了这个对象的所有部分,接下来要考虑的是如何把这些部分组织起来

  • 方式一:用三个变量,每个变量由三部分组成,分别是桌子,状态,具体的状态名称,如下所示

    • 每个变量名有重复的部分,DESK_STATUS_,也就是存在冗余

    • 如果要增一个新的状态值,需要重复前面的2部分,为了方便,你肯定会复制上一个,然后再修改最后一部分的状态名称,而且你还要付出更多的精力去进行核对,由于有一定的相似度,所以很容易存在错位、失手的可能

    • 由于变量名前面的2部分重复,在阅读时无法更快地定位到不同的的部分,体验上容易产生疲劳和失误

DESK_STATUS_WAITING = 1
DESK_STATUS_PLAYING = 2
DESK_STATUS_OVER = 3
  • 方式二:冗余的部分写一次就可以了

    • 多了一行代码,解决了上面的问题,这就是封装了啦
class DeskStatus:
        WAITING = 1
        PLAYING = 2
        OVER = 3

场景1

定义的状态有很多时,由于不小心,发生了重复(有两个值为11),然后需求是每个状态的值都不能重复

class DeskStatus:
        WAITING = 1
        PLAYING = 2
        ...
        XX = 11
        YY = 12
        ... 
        ZZ = 11
        ...     
        OVER = 32

使用@unique装饰器,可以提前发现问题,降低调试成本

from enum import Enum, unique

@unique
class DeskStatus(Enum):
    WAITING = 1
    PLAYING = 2  # PLAYING和OVER的值都为2,抛出ValueError异常
    OVER = 2


ValueError: duplicate values found in <enum 'DeskStatus'>: OVER -> PLAYING

场景2

为模块内部的状态定义的枚举值,这些枚举值的具体作用不需要与模块外部的使用者进行协商,也不需要考虑数据的传输效率。

在这样的情况下,可以不用关注枚举值是1, 2, 3还是4, 5, 6又或者是a, b, c。

使用auto()函数,进一步减少不必要工作量

from enum import Enum, unique, auto

@unique
class DeskStatus(Enum):
    WAITING = auto()
    PLAYING = auto()
    OVER = auto()


DeskStatus.WAITING.value
1
DeskStatus.PLAYING.value
2
DeskStatus.OVER.value
3

从上面的例子,你可以看出,auto()的起始值是1,然后每次+1,它的实际规则也确实是这样子 如果auto()默认的生成规则不满足你的需求,你可以重写_generate_next_value_方法

from enum import Enum, unique, auto

@unique
class DeskStatus(Enum):
    def _generate_next_value_(name, start, count, last_values):
        # last_values 是个list,保存的是已生成的枚举值,如:['A', 'B']
        # count 是len(last_values)的值,如:2
        # start 是开始值,始终为1
        # name 是枚举值的名称,如:WAITING
        # 也就是说你定制的auto()生成规则,会受限于这几个参数的值
        return chr(count + 65)
    WAITING = auto()
    PLAYING = auto()
    OVER = auto()


DeskStatus.WAITING.value
'A'
DeskStatus.PLAYING.value
'B'
DeskStatus.OVER.value
'C'

场景3

通过名称获取值

from enum import Enum, unique

@unique
class DeskStatus(Enum):
    WAITING = 1
    PLAYING = 2
    OVER = 3


DeskStatus.WAITING.value
1

如果你觉得DeskStatus.WAITING.value后面的.value敲起来太麻烦,使用IntEnum就可以省掉,从IntEnum的名称可以看出,仅限int类型的枚举值

from enum import IntEnum, unique

@unique
class DeskStatus(IntEnum):
    WAITING = 1
    PLAYING = 2
    OVER = 3


1 == DeskStatus.WAITING
True

场景4

你现在接受到了一个数值,你想知道什么数值是什么含义,也就是这个数值对应的名称

from enum import Enum, unique

@unique
class DeskStatus(Enum):
    WAITING = 1
    PLAYING = 2
    OVER = 3


DeskStatus(1).name
'WAITING'

场景5

你现在需要给前端的同学提供一个接口,这个接口包括了所有的枚举值以及相应的名称

使用list()来对UpdateType进行转换

from enum import Enum, unique

@unique
class UpdateType(Enum):
    NORMAL = 1
    FORCE = 2


[(item.name, item.value) for item in list(UpdateType)]
[('NORMAL', 1), ('FORCE', 2)]

为什么可以有item.name和item.value,因为它是个对象,可以有name和value属性,在这个场景下看出来Enum的优势了吧,而且这样取值,可读性非常好

可能你会说NORMAL这样的名称在实际的开发中不实用,因为它只能按变量名的规则来定义,不能有空格和中文,我们可以通过改变枚举值的类型来进行扩展

from enum import Enum, unique

@unique
class UpdateType(Enum):
    NORMAL = (1, '常规更新')
    FORCE = (2, '强制更新')


[item.value for item in list(UpdateType)]
[(1, '常规更新'), (2, '强制更新')]

上面的代码,你有没有发现,for循环里取值的语法更简单了

这时候的NORMAL,我们可以理解为它是一个小组长,它有1和常规更新这2个小组员,需要要找这些小组员,找到它就行,如下所示

UpdateType.NORMAL.value[0]
1
UpdateType.NORMAL.value[1]
'常规更新'

如果你说还需要通过方括号加索引的方式去取枚举值,比原来麻烦了一些,虽然解决了一个问题,但又引入了新的问题 只要增加6个用于辅助的类方法,并提升到一个父类中

from enum import Enum, unique

class EasyEnum(Enum):

    @classmethod
    def get_name_by_value(cls, value):
        for item in list(cls):
            if value == item.value[0]:
                return item.name
        return None

    @classmethod
    def get_name_by_annotation(cls, annotation):
        for item in list(cls):
            if annotation == item.value[1]:
                return item.name
        return None

    @classmethod
    def get_value_by_name(cls, name):
        for item in list(cls):
            if name == item.name:
                return item.value[0]
        return None

    @classmethod
    def get_value_by_annotation(cls, annotation):
        for item in list(cls):
            if annotation == item.value[1]:
                return item.value[0]
        return None

    @classmethod
    def get_annotation_by_name(cls, name):
        for item in list(cls):
            if name == item.name:
                return item.value[1]
        return None

    @classmethod
    def get_annotation_by_value(cls, value):
        for item in list(cls):
            if value == item.value[0]:
                return item.value[1]
        return None


@unique
class UpdateType(EasyEnum):

    NORMAL = (1, '常规更新')
    FORCE = (2, '强制更新')


UpdateType.get_name_by_value(1)
'NORMAL'
UpdateType.get_name_by_annotation('常规更新')
'NORMAL'

UpdateType.get_value_by_name('NORMAL')
1
UpdateType.get_value_by_annotation('常规更新')
1

UpdateType.get_annotation_by_name('NORMAL')
'常规更新'
UpdateType.get_annotation_by_value(1)
'常规更新'

场景6

检查一个枚举值、枚举值的名称、枚举值的注解是否合法

继续扩展class EasyEnum

class EasyEnum(Enum):

    ...

    @classmethod
    def values(cls):
        return [item.value[0] for item in list(cls)]

    @classmethod
    def annotations(cls):
        return [item.value[1] for item in list(cls)]   

    @classmethod
    def names(cls):
        return [item.name for item in list(cls)]    


1 in UpdateType.values()
True
'常规更新' in UpdateType.annotations()
True
'NORMAL' in UpdateType.names()
True

场景7

根据不同处理结果返回不同的code以及message

@unique
class Login(EasyEnum):

    SUCCESS = (100, '登录成功')
    USERNAME_OR_PASSWORD_ERROR = (200, '用户名或密码有误')
    EXCEED_LIMIT_TIMES_PER_DAY = (201, '您已被限制登录')    


# 伪代码
def login(request):

    if not check_password:
        return Login.USERNAME_OR_PASSWORD_ERROR.value  

    if not check_limit_times_per_day:
        return Login.EXCEED_LIMIT_TIMES_PER_DAY.value

    return Login.SUCCESS.value  # (100, '登录成功')

这样的代码有没有感觉很清爽,像在读报纸一样

login函数里发生了的事件一目了然,事件失败后的返回值只需要可读性很强的一行代码即可,完全不会因为要返回code和message这2个东西而变得很难维护

对于下面这样的使用逗号来分隔code和message,你觉得哪个更好呢

# 伪代码
def login(request):

    ...

    return LOGIN_SUCCESS_CODE, LOGIN_SUCCESS_MESSAGE  # (100, '登录成功')

运行环境

  • Python:3.6
  • 个人计算机:iMac
  • 处理器:2.9GHz 四核
  • 内存:8GB
  • software:Jupyter

返回首页