我们为什么要用枚举类?从产品经理的角度,手把手带你走进enum的神奇世界

枚举类作为在项目规模扩大时必须使用的类,其重要性不言而喻。但是有很多时候,由于它的麻烦写法,让很多人容易顺手写个常量或者字符串来代替其语义。我们说编程也是需要语义化编程,也就是当我们编程完毕后,代码本身就像注释一般,可以也很容易的让我们理解代码的含义。

因此,这里,我们从产品经理的角度,分析为什么要使用枚举类的3大需求,使用方法以及2个扩展特性,并在最后进行一个深入的源码拓展。

学习任何一个东西,尤其是语言,不仅仅弄清是什么,更要弄清为什么,尽管我们可能花费更多的时间思考,但是这样记的更牢,理解的更透彻。学习英语是如此,学习编程,也是如此。

1. 不使用枚举类的阶段

枚举类在初学者看来是一个相当鸡肋的类。首先,在我们进行选择、比较的时候,我们更喜欢直接赋值变量,以int或者string类型的最多,比如当我们定义一个学生类的性别的时候,我们更喜欢使用male 或者female来进行赋值。

class student:
	def __init__(self):
		self.gender="male"  # or female
	...

或者当我们进行判断时,我们又长又乱的if-else语句其判断条件都是手写的赋值,这样一方面难以理解其含义,另一方面,你很难控制你的data_num赋值时能够赋予他合法的值,而且执行无误,因为我们总会有一个else兜底。

if data_num=1:
	print()
elif data_num=2:
	write()
elif data_num=3:
	exec()
...
else:
	unexception()

因此,我们可能有以下3个需求需要用到枚举:

  1. 当我们需要一个固定的变量时,我们能够很好的记住所有的选项。
  2. 当我们赋予一个可能具有多个离散值的变量时,我们希望它是合法并且符合预期的。
  3. 当我们阅读到一个具有多个离散值的变量时,我们需要能够非常容易的理解其含义。

2. 有枚举思想的阶段

有了这些需求,程序员就开始动脑筋了,我们该如何能够实现刚才的需求呢?作为天才的你一定发现了,我们可以使用python中的字典或者类来实现刚才的功能。

2.1 使用字典实现

比如,当我们需要一个颜色枚举的时候,我们就使用字典实现,如下代码:

# 使用字典
Color = {
    'RED': 1,
    'GREEN': 2,
    'BLUE': 3,
}


#  赋值一个图片的元素的例子
class Picture:
    def __init__(self):
        self.sun_color = Color["RED"]
        self.sky_color = Color["BLUE"]
        self.grass_color = Color["GREEN"]

    def update_sun_color(self, color):
        self.sun_color = color


if __name__ == '__main__':
    custom_picture = Picture()
    new_color = Color["GREEN"]
    custom_picture.update_sun_color(new_color)
    print(custom_picture.sun_color)  # 2

那么这样的改变,满足了刚才的几个需求呢?回过头看,我们看到主要满足了第2和第3条需求,第一条需求仍然没有被满足。因为我们需要一个能够有可选项的编码,而不是自己填。因此使用类也可以解决这个问题。

2.2 使用类实现

class Color:
    RED = 1
    GREEN = 2
    BLUE = 3


#  赋值一个图片的元素的例子
class Picture:
    def __init__(self):
        self.sun_color = Color.RED
        self.sky_color = Color.BLUE
        self.grass_color = Color.GREEN

    def update_sun_color(self, color):
        self.sun_color = color


if __name__ == '__main__':
    custom_picture = Picture()
    new_color = Color.GREEN
    custom_picture.update_sun_color(new_color)
    print(custom_picture.sun_color)  # 2

这样看似问题都解决了,也能够可读,也能够候选。

但是其实它还有两个缺陷,第一个是,它的第二条会在比较时发生问题,即如果出现了预期之外的值的时候,没有办法处理,比如我们这里就只有3个颜色,如果我们新的值是5,我们就很难处理。常见的处理方法比如加一个default用来表示其他值,但是有可能是无穷尽个不符合预期的值呢?第二个,这个属性的值是会被改变的,下面一个例子就揭示了一个非常吓人的场景。

if __name__ == '__main__':
    custom_picture = Picture()
    Color.GREEN = 3  # 新加的一句话
    new_color = Color.GREEN
    custom_picture.update_sun_color(new_color)
    print(custom_picture.sun_color)  # 3
    print(custom_picture.sun_color == custom_picture.sky_color)  # 非常诡异的True

上面的例子可以看出,我们只需要增加一句话,太阳和天空的颜色居然是一样的,尽管我们赋值时,以为自己赋予的是绿色!因此,枚举类应运而生了。它将满足上面的三个需求,我们会一一讲解。

3. 使用枚举的阶段

综合上面的例子,我们可以看到,枚举真的非常有必要使用,曾经不使用枚举的我,陷入了很多代码的巨坑!回归正题,我们首先看一个枚举类的例子。

from enum import Enum


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

我们只需要继承一个枚举类,就可以完全的避免刚才普通类出现的问题,当你再次非法赋值的时候,你会收到如下的警告:

raise AttributeError('Cannot reassign members.')
AttributeError: Cannot reassign members.

因此就可以避免非法的重新赋值的场景,能够满足1,2,3条需求。当然你也可以用以下语句更加便捷的创建一个枚举类:

Color = Enum("Color",('RED','GREEN ','BLUE '))

3.1 枚举类的使用与特性

刚才我们主要见识了枚举类和一般类的区别和样子,我们现在看看如何正确使用枚举类,还是以刚才的例子:

from enum import Enum
# 创建一个枚举类
class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2
    blue = 2  # 别名
#调用枚举成员的 3 种方式
print(Color.RED)  # Color.RED
print(Color['RED'])  # Color.RED
print(Color(1))  # Color.GREEN
#调取枚举成员中的 value 和 name
print(Color.RED.value)  # 0
print(Color.RED.name)  # RED
#遍历枚举类中所有成员的 2 种方式
for color in Color:
    print(color)  # Color.RED  Color.GREEN  Color.BLUE 
for color in Color.__members__:
    print(color)  # RED  GREEN  BLUE  blue 

从上面我们可以看到,枚举类甚至可以做出不同名,同值的情况,第二个名称会被认定为别名,在普通遍历时,是不会被看到的。

当我们幻想着有其他非法值时print(Color(3)),它会和字典一样自动弹出异常:

raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 3 is not a valid Color

3.2 枚举类的扩展特性

通过刚才的讲解,我们知道枚举类除了满足我们的3大需求外,展现出两个特性:不唯一性不可比较性。如果想要打破这两个特性,我们就不得不使用一些手段了。

1. 唯一性

如果我们想要让枚举变得唯一,只需要增加一个独一性的装饰器即可:

# 创建一个唯一性的枚举类
from enum import Enum, unique


@unique
class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2
    blue = 2

此时就会报错:

    (enumeration, alias_details))
ValueError: duplicate values found in <enum 'Color'>: blue -> BLUE

2. 可比较性

枚举的成员可以通过 is 同一性比较或通过 == 等值比较,但是不能够比较大小,其原因就是我们这个枚举类非常的基础,只是从Object类继承,而Object类确实就只能进行等于比较,不能进行大小比较,不过好在我们又更加细致的类IntEnum,当你需要进行数值比较的时候,可以实现这个类:

from enum import IntEnum


class Shape(IntEnum):
    circle = 1
    square = 2


class Request(IntEnum):
    post = 1
    get = 2


print(Shape.circle == 1)  # True
print(Shape.circle < 3)  # True
print(Shape.circle == Request.post)  # True
print(Shape.circle >= Request.post)  # True

深入了解枚举阶段

看到枚举类这么神奇,我们就不想看一看枚举到底是如何实现的么?下面给出一个自我实现的枚举和官方的部分源码,由于这里是更加深层次的理解,详情请看《python中enum模块源码的详细分析》。

自我实现一个枚举

就像我们改进字典的缺陷一样,我们可以基于字典构建一个枚举,这里还可以使用__prepare__魔术方法返回一个类字典实例,但是这个枚举仍然没有解决可以重新赋值的问题。

class _Dict(dict):
    def __setitem__(self, key, value):
        if key in self:
            raise TypeError('Attempted to reuse key: %r' % key)
        super().__setitem__(key, value)


class MyMeta(type):
    @classmethod
    def __prepare__(metacls, name, bases):
        d = _Dict()
        return d


class Enum(metaclass=MyMeta):
    pass


class Color(Enum):
    red = 1
    red = 1  # TypeError: Attempted to reuse key: 'red'

官方源码解析

官方的代码就强大和复杂的多,下面是部分类及其实现,我们仍然能够看到刚才的想法的影子。另一个我们可学习的是,当我们一个类特别的复杂时,我们需要将其分解为一个有一个小类,注意的是,类的方法只能操纵类原有的属性,如果要在类的更上一层进行操作和管理时,可以再创建一个更高级的管理者来管理我们所需要的类;而当你需要将类进入到不同的场景以细化时,可以使用继承出子类进行操作,省去很多重复代码。

class _EnumDict(dict):
    def __init__(self):
        super().__init__()
        self._member_names = []
        ...

    def __setitem__(self, key, value):
        ...
        elif key in self._member_names:
            # descriptor overwriting an enum?
            raise TypeError('Attempted to reuse key: %r' % key)
        ...
        self._member_names.append(key)
        super().__setitem__(key, value)

class EnumMeta(type):
    @classmethod
    def __prepare__(metacls, cls, bases):
        enum_dict = _EnumDict()
        ...
        return enum_dict

class Enum(metaclass=EnumMeta):
    ...
发布了232 篇原创文章 · 获赞 547 · 访问量 51万+

猜你喜欢

转载自blog.csdn.net/qq_35082030/article/details/104910946