将 __prepare__ 用于枚举...有啥问题?

Posted

技术标签:

【中文标题】将 __prepare__ 用于枚举...有啥问题?【英文标题】:Using __prepare__ for an Enum ... what's the catch?将 __prepare__ 用于枚举...有什么问题? 【发布时间】:2017-10-06 21:30:28 【问题描述】:

Python 的enum.Enum 的声明式使用需要提供值,而在枚举的最基本用例中,我们实际上并不关心名称和值。我们只关心哨兵本身。最近看了一篇related 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 来帮助解决 omitting values 的问题,但界面仍然很奇怪 - 您需要为每个成员指定 auto() 值,并从不同的成员继承修复__repr__的基础:

class Color(NoValue):
    red = auto()
    blue = auto()
    green = auto()

知道为标准库选择的实现已经投入了许多工时和非常谨慎的工作,因此之前演示的可以说更 Pythonic 的声明性枚举版本不能正常工作肯定是有原因的。

我的问题是,提议的方法有哪些问题和失败模式,为什么决定反对(或类似的方法) - 而 Python 3.6 中包含 auto 功能?

【问题讨论】:

【参考方案1】:

defaultdict 用作枚举的命名空间有几个缺陷:

无法访问除其他枚举成员/方法之外的任何内容 错别字创建新成员 失去保护形式 _EnumDict 命名空间: 覆盖成员 覆盖方法 较新的_generate 方法

还有最重要的:

它不起作用

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

然而,声明一个不带值的名称并非不可能——aenum1 包允许它通过一些保护措施:

神奇的自动行为仅在定义成员时出现(一旦定义了正常方法,它就会关闭) propertyclassmethodstaticmethod 默认被排除在外,但可以包含它们和/或排除其他全局名称

不过,这种行为对于 stdlib 来说太神奇了,所以如果你想要它以及其他一些增强/改进2,你必须使用aenum

一个例子:

from aenum import AutoEnum

class Color(AutoEnum):
    red
    green
    blue

__repr__ 仍然显示创建的值。

--

1 披露:我是Python stdlib Enumenum34 backport 和Advanced Enumeration (aenum) 库的作者。

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

MultiValueEnum --> 多个值可以映射到一个名称(不是别名) NoAliasEnum --> 具有相同值的名称不是别名(想想扑克牌) OrderedEnum --> 成员在定义上是可排序的 UniqueEnum --> 不允许使用别名

【讨论】:

有趣,谢谢!我刚刚简要浏览了AutoEnum 的实现,但使用_generate_next_value_ 的东西很快就深入了。可以说元类使用与我的问题中描述的相同的基本技巧,还是完全不同的实现? @wim:类似:defaultdict 使用__missing__ 方法,而_EnumDict 捕获__getitem__ 查找并从那里处理。 @wim:aenum 的复杂性在于支持 Python 2.x 系列。几天前我打开了Enum 的stdlib 版本,感觉就像呼吸了新鲜空气! 嗯,文档中没有对 aenum 的单一引用。对于可能希望更神奇的用户来说,这似乎是一个很好的参考。 @JimFasarakisHilliard:Python 文档不习惯引用外部项目(我相信只有几个例外)。不过也许是维基……【参考方案2】:

您可能对可以使用多个参数创建枚举感兴趣:

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] 已在某处定义(即 from enum import [auto]),它就是普通 Python,不会与其他语言或其工具链发生冲突。

简而言之:类定义将这些“变量”解释为查找:

class A(object):
    a

但是对于enum,它们应该被解释为分配吗?这个用例根本不被考虑"special enough to break the rules"。

【讨论】:

是的,我知道函数式接口(我不喜欢它)......这就是问题开始的原因:“Declarative 使用 Python 的枚举...”。为相关讨论的链接 +1,不过

以上是关于将 __prepare__ 用于枚举...有啥问题?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 python 2 中为元类替换 __prepare__

enum枚举

Python元类__prepare__方法深入理解

在编写设备驱动程序时,linux中的__iomem有啥用?

Python 的 enum 模块源码分析

有啥方法可以为我的 _Layout 创建特殊的控制器?