使用__prepare__创建枚举类型...有什么需要注意的地方吗?

10
Python的enum.Enum的声明式用法需要提供值,但在枚举的最基本用例中,我们实际上并不关心名称和值。我们只关心标志本身。最近阅读一个相关Q&A后,我意识到可以使用枚举元类的__prepare__方法来获得这种声明:
class Color(Enum):
    red
    blue
    green

实现让事情变得如此干燥的方法其实相当简单:

from collections import defaultdict

class EnumMeta(type):
    @classmethod
    def __prepare__(meta, name, bases):
        return defaultdict(object)

    def __new__(cls, name, bases, classdict):
        classdict.default_factory = None
        return type.__new__(cls, name, bases, classdict)

class Enum(metaclass=EnumMeta):
    pass

在Python 3.6中,提供了enum.auto来帮助解决省略值的问题,但接口仍然很奇怪 - 您需要为每个成员指定auto()值,并从不同的基类继承,以修复__repr__
class Color(NoValue):
    red = auto()
    blue = auto()
    green = auto()

我知道标准库的实现已经花费了大量的时间和精力,所以可能有一些原因导致早期展示的Pythonic风格的声明性枚举版本不能正常工作。我的问题是,这种方法的问题和故障模式是什么,为什么会决定反对(或类似的决定) - 而选择在Python 3.6中包括auto特性?
2个回答

8
有几个缺陷会导致defaultdict成为枚举的命名空间:
  • 无法访问除其他枚举成员/方法之外的任何内容
  • 拼写错误会创建新的成员
  • 失去了对_EnumDict命名空间的保护:
    • 覆盖成员
    • 覆盖方法
    • 较新的_generate方法

而最重要的是:

  • 它将不起作用

为什么不起作用?不仅__prepare__可以在命名空间字典上设置属性,命名空间字典本身也可以这样做——_EnumDict就这样做了:_member_names,即应该是成员的所有属性的列表。

然而,声明一个没有值的名称的目标并不是不可能的——aenum1包允许这样做,并带有一些保障:

  • 只有在定义成员时才存在自动行为(一旦定义了普通的方法,它就会关闭)
  • propertyclassmethodstaticmethod默认情况下被排除在外,但可以包含它们和/或排除其他全局名称

然而,这种行为被认为对stdlib来说太神奇了,所以如果你想使用它以及其他一些增强/改进2,你需要使用aenum

一个例子:

from aenum import AutoEnum

class Color(AutoEnum):
    red
    green
    blue
< p > __repr__ 仍然显示已创建的值。

--

1声明:我是Python标准库Enumenum34回溯高级枚举(aenum库的作者。

2NamedConstant(就像它所说的一样;)、NamedTuple(基于元类,具有默认值等功能),以及一些内置的枚举:

  • MultiValueEnum --> 多个值可以映射到一个名称(不是别名)
  • NoAliasEnum --> 具有相同值的名称不是别名(类似于纸牌游戏)
  • OrderedEnum --> 成员在定义时是可排序的
  • UniqueEnum --> 不允许使用别名

有趣,谢谢!我刚刚简单地浏览了AutoEnum的实现,但是它很快就深入到_generate_next_value_的内容中。可以说元类使用了与我的问题描述中相同的基本技巧吗?还是完全不同的实现方式? - wim
@wim:类似的,defaultdict 使用 __missing__ 方法,而 _EnumDict 捕获 __getitem__ 查找并从那里处理它。 - Ethan Furman
1
@wim:aenum 中的许多复杂性都是为了支持 Python 2.x 系列。我几天前打开了 Enum 的标准库版本,感觉就像呼吸新鲜空气一样! - Ethan Furman
哼,文档中没有一个关于aenum的参考。对于可能希望有点更神奇的东西的用户来说,它似乎是一个很好的参考资料。 - Dimitris Fasarakis Hilliard
1
@JimFasarakisHilliard:Python文档通常不习惯引用外部项目(我相信只有几个例外)。也许可以参考维基百科... - Ethan Furman

1

你可能会感兴趣的是,你可以使用多个参数来创建枚举:

from enum import Enum

class NoValue(Enum):
    def __repr__(self):
        return '<%s.%s>' % (self.__class__.__name__, self.name)

Color = NoValue('Color', ['red', 'green', 'blue'])  # no need for "auto()" calls

那样你就不必使用 auto 或其他任何东西(如 __prepare__)。
为什么决定不采用这个(或类似的)功能,而是将自动特性包含在Python 3.6中?这一点已经在Python问题跟踪器上广泛讨论过(尤其是bpo-23591),以下是反对意见的摘要: Vedran Čačić:
这是一些基本的东西:它打破了类体是命令套件的承诺,其中Python语句(如分配)具有其通常的语义。 Raymond Hettinger:
只要[auto]已经在某个地方定义过了(即从枚举导入[auto]),它就是正常的Python,并且不会与语言或其工具链发生冲突。
简而言之:类定义将这些“变量”解释为查找。
class A(object):
    a

但是对于enum来说,它们应该被解释为赋值吗?这种用例并没有被认为"足够特殊以打破规则"


1
是的,我知道函数式接口(而且我不喜欢它)...这就是为什么问题开头会像这样:“Python枚举的声明式使用...”。谢谢提供相关讨论链接的点赞。 - wim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接