• 自从Python有了枚举类型后
  • 发布于 2个月前
  • 391 热度
    0 评论
  • 奥特蛋
  • 1 粉丝 48 篇博客
  •   
背景
现在的 Python 项目越来越大,一个模型可能就有十几万行。以前没有枚举的时候我们是常量满天飞,Python-3.4 给我们带来了对枚举类型的支持,新的编码方式不管是在可读性、安全性都有不错的提升。要讲清楚枚举类型的好处,还要从没有枚举类型的时候社区的编码风格说起,下面我们一步步体验枚举类型。

以前的写法
大多数情况下程序员定义枚举其实就是为了定义常量,这也就是我以前觉得 Python 没有枚举类型也没事;既然它已经有了我们还是看一下它的优点和不足。假设我们要定义一些颜色与日期相关的常量、还有一个用来检查给定日期是不是工作日的小函数,以前我们的代码可能像是这样。
#/usr/bin/env python3
# -*- coding: utf8 -*-

# RGB 常量
RED = 1
GREEN = 2
BLUE = 3

# 周一到周天常量
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7

def is_work_day(weekday):
    """检查 weekday 是不是工作日
    Paramter:
    ---------
    weekday: int
        星期几
    
    Return:
    -------
        bool

    Example:
        is_weekend(1) -> True
        is_weekend(6) -> False
        is_weekend(7) -> False
    """
    # 检查是不是工作日
    return weekday in (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY)

def main():
    # 注意传递的是 RED 这里会打印 True , 明显不应该的
    print(is_work_day(RED))

if __name__ == "__main__":
    main()
运行结果
python3 main.py 
True
运行结果为 True ,也就是说按现在的这套逻辑“红色”和“星期一”是一样的。这个就是常量最为致命的一个点“非类型安全”,另一个问题就是他们的打印输出都是整数,这个就使得输出的可读性不强,最后一个就写法上也不优雅(在 Python 中不优雅就是有罪)。

枚举类型输出更加友好
使用枚举最为直观的收益就是它的打印更加友好(内部通过重载 __repl__ 自动实现),下面看例子。
#堆代码 www.duidaima.com
#/usr/bin/env python3
# -*- coding: utf8 -*-

from enum import Enum

# RGB 常量
RED = 1
GREEN = 2
BLUE = 3

class Colors(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print("const value {}\nenum value {}".format(RED, Colors.RED))
从输出结果我们可以看到确实是更加的友好了。
python3 main.py
# 一个输出是整数,另一个看起来就是见名知意
const value 1
enum value Colors.RED
枚举类型能做到类型安全
这里是多数常量相关 Bug 的源头,在使用常量的年代,我们把“红色”和“周一”都设置成了 1 ,当我们在比较操作时这两个常量会相等,但是业务逻辑上我们并不希望他们相等。
#/usr/bin/env python3
# -*- coding: utf8 -*-

from enum import Enum

# RGB 常量
RED = 1
...
# 周一到周天常量
MONDAY = 1
...

# 这样比较的话它一定是 True , 可能我们在业务逻辑上并不希望它这样
print(RED == MONDAY)
使用枚举的话这种 Bug 就不会发生了,下面看使用枚举时的代码。
#/usr/bin/env python3
# -*- coding: utf8 -*-

from enum import Enum

# RGB 常量
RED = 1
GREEN = 2
BLUE = 3

class Colors(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

class Weekdays(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7
# 下面两个比较都会返回 False ,原因是类型不同就直接返回 False 都不用比较值

print("RED == Colors.RED => {}".format(RED == Colors.RED))
print("Colors.RED == Weekdays.MONDAY => {}".format(Colors.RED == Weekdays.MONDAY))
运行结果
python3 main.py

RED == Colors.RED => False
Colors.RED == Weekdays.MONDAY => False
枚举类型让代码更加优雅
Python 中许多看起来优雅的语法多数是运算符重载的功劳。枚举类型也在这方面做足了工作,下面看例子。使用常量时,我们检查给定的日期是不是工作日,代码如下:
def is_work_day(weekday):
    # 检查是不是工作日
    return weekday in (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY)
看这段代码的时候我还要用脑子想一下它是通过什么方式在“工作日”和“常量”之间建立联系的。如果我们用枚举类型可以说是所见即所得,总的来讲对人类更加友好。
from enum import Flag

class Weekdays(Flag):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 4
    THURSDAY = 8
    FRIDAY = 16
    SATURDAY = 32
    SUNDAY = 64

def is_work_day_enum(weekday):
    # 这里会使用位运算来进行比较
    return weekday == Weekdays.MONDAY | Weekdays.TUESDAY | Weekdays.WEDNESDAY | Weekdays.THURSDAY | Weekdays.FRIDAY

枚举类型性能会差上不少
前面我们说过许多枚举类型的好处,宏观上集中在两点 1、更加安全以减小我们写出 Bug 的可能性,2、语法上更加优雅。它的缺点就是性能会差不少,先看测试结果吧,测试代码后面会给到。执行 100w 次比较,看常量与枚举的耗时情况,详细的代码如下。
#/usr/bin/env python3
# -*- coding: utf8 -*-

"""
枚举类型的常见使用方式与性能测试
作者:蒋乐兴(初代庄主)
时间:2023-01
"""

from enum import Enum, Flag
from datetime import datetime

# RGB 常量
RED = 1
GREEN = 2
BLUE = 3

# 周一到周天常量
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7


class Colors(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# 使用 Flag 以它支持位操作,这样会更快一些(相比 Enum)
class Weekdays(Flag):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 4
    THURSDAY = 8
    FRIDAY = 16
    SATURDAY = 32
    SUNDAY = 64


def is_work_day(weekday):
    """检查 weekday 是不是周末
    Paramter:
    ---------
    weekday: int
        周几
    
    Return:
    -------
        bool

    Example:
        is_weekend(1) -> True
        is_weekend(6) -> False
        is_weekend(7) -> False
    """
    # 检查是不是工作日
    return weekday in (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY)


def is_work_day_enum(weekday):
    # 第一种写法
    #return weekday in (Weekdays.MONDAY, Weekdays.TUESDAY, Weekdays.WEDNESDAY, Weekdays.THURSDAY, Weekdays.FRIDAY)
    
    # 第二种写法,会更加优雅,但是性能会更加差
    # 这里会使用位操作来进行比较
    return weekday == Weekdays.MONDAY | Weekdays.TUESDAY | Weekdays.WEDNESDAY | Weekdays.THURSDAY | Weekdays.FRIDAY


def main():
    """比较在特定算法下常量和枚举的性能差异
    """
    choices = [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
    start = datetime.now()
    for i in range(1000000):
        index = i % 7
        is_work_day(choices[index])
    end = datetime.now()
    print("使用常量耗时 {}".format(end - start))

    choices = [Weekdays.MONDAY, Weekdays.TUESDAY, Weekdays.WEDNESDAY, Weekdays.THURSDAY, Weekdays.FRIDAY, Weekdays.SATURDAY, Weekdays.SUNDAY]
    start = datetime.now()
    for i in range(1000000):
        index = i % 7
        is_work_day_enum(choices[index])
    end = datetime.now()
    print("使用枚举类型耗时 {}".format(end - start))


if __name__ == "__main__":
    main()
运行结果如下:
python3 main.py
使用常量耗时 0:00:00.170633
使用枚举类型耗时 0:00:02.534917

100w 次比较用时 2s 多,多数程序代码应该也不会有这么高的要求。我这边建议还是尽可能的使用枚举,毕竟不写 Bug 才是第一位的,至于性能问题不用过早的优化(除非是特别明显的性能问题)。 

用户评论