关于枚举:python中的枚举

Enumerations in python

本问题已经有最佳答案,请猛点这里访问。

Duplicate:
What’s the best way to implement an ‘enum’ in Python?

在Python中,公认的枚举方式是什么?

例如,现在我正在写一个游戏,希望能够移动"向上"、"向下"、"左"和"右"。我使用字符串是因为我还没有弄清楚枚举在Python中是如何工作的,所以我的逻辑中到处都是这样的东西:

1
2
3
def move(self, direction):
    if direction =="up":
        # Do something

我想把"up"换成类似Directions.up的东西


更新1:python 3.4将有一个内置的设计良好的枚举库。这些值总是知道它们的名称和类型;有一个整数兼容模式,但建议新使用的默认值是singleton,与任何其他对象都不相同。

更新2:自从写了这个之后,我意识到枚举的关键测试是序列化。其他方面可以稍后重构,但如果枚举进入文件/连接,请预先问问自己,如果它是由旧/新版本反序列化的(可能支持一组不同的值),会发生什么情况…

如果您确定需要枚举,那么其他人已经回答了如何进行枚举。但让我们看看你为什么想要它们?了解动机有助于选择解决方案。

  • 原子值-在C语言中,小数字很容易传递,字符串则不容易传递。在Python中,"up"这样的字符串非常适合多种用途。而且,任何结果只有一个数字的解决方案对于调试来说都是糟糕的!

  • 有意义的价值观-在C语言中,你经常需要处理现有的魔法数字,只需要一些语法糖。这里不是这样的。但是,您可能希望与其他有意义的信息关联方向,例如(dx,dy)向量-更多信息见下文。

  • 类型检查-在C中,枚举有助于在编译时捕获无效值。但python通常更喜欢牺牲编译器检查来减少输入。

  • 自省(不存在于c枚举中)-您希望知道所有有效值。

    • 完成-编辑器可以显示可能的值并帮助您键入它们。

已兑现的字符串(aka符号)

因此,在Python解决方案的浅色方面,只需使用字符串,并可能拥有所有有效值的列表/集合:

1
2
3
4
5
6
7
8
DIRECTIONS = set(['up', 'down', 'left', 'right'])

def move(self, direction):
    # only if you feel like checking
    assert direction in DIRECTIONS
    # you can still just use the strings!
    if direction == 'up':
        # Do something

请注意,调试器会告诉您函数是以"up"作为参数调用的。如果direction实际上是0的任何解决方案都比这更糟糕!

在Lisp语言家族中,这种用法被称为符号-原子对象可以像数字一样简单地使用,但带有文本值。(准确地说,符号是类似于字符串的,但是一种单独的类型。但是,python通常使用常规字符串,其中lisp将使用符号。)

命名空间字符串

您可以将'up'0更好的想法与其他解决方案结合起来。

如果要捕获(运行时)拼写错误:

1
2
3
UP = 'up'
...
RIGHT = 'right'

如果你想坚持输入前缀来完成任务,把上面的内容放到一个类中:

1
2
3
4
class Directions:
    UP ="up"
    ...
    RIGHT ="right"

或者只是在一个单独的文件中,使其成为一个模块。

一个模块允许懒惰的用户执行from directions import *来跳过前缀,无论您认为这是加还是减…(如果我经常使用,我个人就不愿意被迫输入Directions.UP)。

具有功能的对象

如果每个值都有相关的有用信息/功能怎么办?"右"不只是4个任意值中的一个,它是x轴上的正方向!

如果你所做的事情是:

1
2
3
4
5
6
7
8
9
def move(self, direction):
    if direction == 'up':
        self.y += STEP
    elif direction == 'down':
        self.y -= STEP
    elif direction == 'left':
        self.x -= STEP
    elif direction == 'right':
        self.x += STEP

你真正想写的是:

1
2
3
def move(self, direction):
    self.x += direction.dx * STEP
    self.y += direction.dy * STEP

就这样!

因此,您希望将其放入以下两个实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Written in full to give the idea.
# Consider using collections.namedtuple
class Direction(object):
    def __init__(self, dx, dy, name):
        self.dx = dx
        self.dy = dy
        self.name = name
    def __str__(self):
        return self.name

UP = Direction(0, 1,"up")
DOWN = Direction(0, -1,"down")
LEFT = Direction(-1, 0,"left")
RIGHT = Direction(1, 0,"right")

或者只是上课:

1
2
3
4
5
6
7
8
9
10
11
12
class Direction(object):
    pass

class Up(Direction):
    dx = 0
    dy = 1

...

class Right(Direction):
    dx = 1
    dy = 0

记住,在Python中,类也是对象(不同于任何其他对象),您可以比较它们:direction == Up等。

通常,实例可能更干净,但如果枚举的概念具有某种层次关系,有时直接用类建模非常好。


1
2
3
4
5
class Directions:
    up = 0
    down = 1
    left = 2
    right =3


我给了库格尔+1,但另一个更精简的选择是

1
dirUp, dirDown, dirLeft, dirRight = range(4)
      • (一段时间过去了)

所以我在想……我们这里有一个明显的干法冲突,因为我们已经在lhs上指定了四项,然后在rhs上再次指定了四项。如果我们将来添加项目会发生什么?当别人加上它们时会发生什么,也许它们比我们自己更马虎?消除干冲突的一个明显方法是使用枚举本身的列表来分配它们的值:

1
2
3
4
5
6
7
8
>>> enums = ['dirUp', 'dirDown']
>>> for v, k in enumerate(enums):
...     exec(k + '=' + str(v))
...    
>>> print dirDown
1
>>> print dirUp
0

如果你能用exec()来消化,那就好了。如果没有,那么使用另一种方法。无论如何,目前的讨论都是学术性的。但是,这里仍然有一个问题。如果枚举在大量的源代码中使用,并且有其他程序员在dirUpdirDown之间插入一个新值,会怎么样?这将导致痛苦,因为枚举的名称和枚举本身之间的映射将是错误的。记住,即使在最初的简单解决方案中,这仍然是一个问题。

这里,我们有一个新颖的想法,即使用内置的hash()函数确定枚举值作为int,并使用枚举本身的文本名称来确定哈希:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> for k in enums:
...     exec(k + '=' + str(hash(k)))
...
>>> dirUp
-1147857581
>>> dirDown
453592598
>>> enums = ['dirUp', 'dirLeft', 'dirDown']
>>> for k in enums:
...     exec(k + '=' + str(hash(k)))
...
>>> dirUp
-1147857581
>>> dirDown
453592598
>>> dirLeft
-300839747
>>>

注意,我们在dirUpdirDown之间插入了一个新值,即dirLeft,前两个映射值没有改变。

我可以在自己的代码中使用这个。感谢OP发布问题。

      • (又过了一段时间)

贝尼·切尔尼亚夫斯基·帕斯金发表了一些非常好的评论:

  • python的默认hash()在不同的平台上不稳定(对于持久性应用程序是危险的)
  • 碰撞的可能性总是存在的。

我倾向于同意这两种观点。他的建议是使用字符串本身(我真的很喜欢使用值的自记录行为)作为散列,因此代码变成以下代码(注意,我们使用集合而不是列表来强制唯一性):

1
2
3
4
5
>>> items=('dirUp','dirDown','dirLeft','dirRight')
>>> for i in items:
        exec('{}="{}"'.format(i,i))
>>> dirDown
'dirDown'

将它们放在名称空间中也很简单,以避免与其他代码冲突:

1
2
3
4
5
6
>>> class Direction():
        for i in ('dirUp','dirDown','dirLeft','dirRight'):
            exec('{}="{}"'.format(i,i))

>>> Direction.dirUp
'dirUp'

他提到的加密散列的长度可以在这里看到:

1
2
3
4
>>> from hashlib import md5
>>> crypthash = md5('dirDown'.encode('utf8'))
>>> crypthash.hexdigest()
'6a65fd3cd318166a1cc30b3e5e666d8f'


collections.namedtuple对象可以提供这样的名称空间:

1
2
3
4
5
6
7
8
>>> import collections
>>> dircoll=collections.namedtuple('directions', ('UP', 'DOWN', 'LEFT', 'RIGHT'))
>>> directions=dircoll(0,1,2,3)
>>> directions
directions(UP=0, DOWN=1, LEFT=2, RIGHT=3)
>>> directions.DOWN
1
>>>

这是简单有效的:

1
2
3
class Enum(object):
  def __init__(self, *keys):
    self.__dict__.update(zip(keys, range(len(keys))))

用途:

1
2
3
4
5
>>> x = Enum('foo', 'bar', 'baz', 'bat')
>>> x.baz
2
>>> x.bat
3


如果您使用的是python 2.6+,那么您可以使用namedtuple。它们的优点是具有固定数量的属性,当需要所有枚举值时,可以像使用元组一样使用它。

为了更好地控制枚举值,可以创建自己的枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def enum(args, start=0):
    class Enum(object):
        __slots__ = args.split()

        def __init__(self):
            for i, key in enumerate(Enum.__slots__, start):
                setattr(self, key, i)

    return Enum()

>>> e_dir = enum('up down left right')
>>> e_dir.up
0
>>> e_dir = enum('up down left right', start=1)
>>> e_dir.up
1

声明__slots__将密封枚举类,不能再为从具有__slots__属性的类创建的对象设置属性。

您的Enum类也可以基于namedtuple,在这种情况下,您还可以获得元组的特性。见namedtuple docs关于namedtuple的子类。