【问题标题】:Support unknown values in Python Enums支持 Python 枚举中的未知值
【发布时间】:2019-05-05 21:26:11
【问题描述】:

我想扩展 Python 枚举以支持未定义的值。

我的用例:我想从 Enums 中受益(例如,能够用说话的名字来寻址/比较已知条目),但我也希望它支持未知值。如果值后面没有名称,str 应该简单地打印值的 str 表示,比较应该失败。

让我给你一个简短的例子,我想要什么:

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name


print(Foo(1)) # prints 'A'
print(Foo(2)) # print 'B'
print(Foo(3)) # prints 'C'
print(Foo(1) == Foo.A) # prints 'true'

print(Foo(4)) # I'd expect '4'
print(Foo(123)) # I'd expect '123'
print(Foo(123) == Foo.A) # I'd expect False

当然最后几行失败了。

有没有办法扩展 Enums,或者是否有另一种简单的 Python 方式? (请不要使用第三方库。)

【问题讨论】:

  • 这与使用枚举相反,枚举应该枚举生成一组已知值。
  • 好的,明白了——还有其他简单的pythonic方法可以实现我的目标吗?
  • Please explain what you need this for -- 很可能有更合适的解决方案。
  • 让我试试。我解析魔法常数(整数)。其中一些是明确定义的,并且有它们的名称,有些则没有。我需要检查其中一些定义明确的整数,并且我想在比较期间使用它们的口语名称(为了代码的可读性)。稍后,我想使用这些值的类型安全表示来生成配置文件。如果实例有一个说话的名字,打印它。如果没有,只需使用其原始整数值。枚举满足这些要求,只要我没有未定义的值。但是有一些。希望对您有所帮助。
  • 我可以看到这种情况正在发生。检查我的答案以获得解决方案。

标签: python inheritance enums


【解决方案1】:

要使这项工作在 Python 的多个值(尤其是 Python 2)上工作,您将需要一个外部库:aenum1。

from aenum import Enum, extend_enum

class Foo(Enum):
    _order_ = 'A B C'   # because Python 2 does not keep order
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name

    @classmethod
    def _missing_(cls, value):
        extend_enum(cls, str(value), (value, ))
        return list(cls)[-1]

这会将未知值作为枚举成员动态添加并返回。


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

【讨论】:

  • 工作完美,但正如我最初的问题中所述,我想避免使用第三方库。
  • @rralf:正如我在回答中指出的那样,如果您要同时支持 Python 2 和 3,那么您将需要第三方库...除非您使用非当然,枚举解决方案或自己重写整个事情。
【解决方案2】:

这是另一个适用于 Python 3.6、3.7 和 3.8 的答案。

它涉及到奇怪的元类黑客,所以要小心。

import enum

class TestMeta(enum.EnumMeta):
    def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
        if names is not None:
            # the enum is being constructed via the functional syntax
            return super().__call__(value, names=names, module=module, qualname=qualname, type=type, start=start)

        try:
            # attempt to get an enum member
            return super().__call__(value, names=names, module=module, qualname=qualname, type=type, start=start)
        except ValueError:
            # no such member exists, but we don't care
            return value

class Test(enum.Enum, metaclass=TestMeta):
    A = 5
    B = 6    

print(Test(5), Test(6), Test(7))

此版本works in Python 2.7,但需要enum 的第三方库(参见 cmets):

class TestMeta(enum.EnumMeta):
    def __call__(cls, value, names=None, module=None, type=None, start=1):
        if names is not None:
            return enum.EnumMeta.__call__(cls, value, names, module, type, start)

        try:    
            return enum.EnumMeta.__call__(cls, value, names, module, type, start)
        except ValueError:
            return value

【讨论】:

  • 看起来是一个很有前途的解决方案。在我的问题中我忘了提到我还需要支持python2,但如果没有进一步的解决方案,我会接受。
  • @rralf,我添加了一个似乎适用于 Python 2 的版本
  • @rralf:Python 2.7 中没有enum,那么使用的是哪个第三方库?
  • @EthanFurman,是的,它实际上并没有......但是导入 some kind of enum 以某种方式起作用。在站点上运行 help(enum) 不会提供有关库版本或任何链接的任何信息。
  • @ForceBru:既然有EnumMeta,它可能就是enum34,它是Python3.4 中包含的Enum 的基本版本。 aenum 库实际上具有最新 Python 中存在的 Enum 的所有功能,以及一些额外的东西(例如 extend_enum())。
【解决方案3】:

可以按照enum.Flag._create_pseudo_member_ 中的示例尝试在运行时使用新成员扩展枚举:

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name

    @classmethod
    def _missing_(cls, value):
        if cls._member_type_ is object:
            # construct a new singleton enum member
            new_member = object.__new__(cls)
            # include the value representation in _name_ because the Enum hash implementation relies on _name_
            new_member._name_ = '%r' % value
            new_member._value_ = value
            # use setdefault in case another thread already created a composite with this value
            new_member = cls._value2member_map_.setdefault(value, new_member)
            return new_member

这种简单的实现将打破在类定义中将Enum 与其他类型混合的可能性,但这当然可以在enum.EnumMeta.__new__ 中的机制之后修复。

正如@chepner 已经评论的那样,这违反了承诺

一旦定义了枚举项,Enum 类就是最终的

(也可以在 enum.EnumMeta.__new__ 中找到)并且预计会破坏某些 Enum 方法的行为,例如 __len__。

【讨论】:

    【解决方案4】:

    对于您的用例,更自然的解决方案是:

    • 获取输入中存在的数值的完整列表(通过解析)
    • 使用这些值和已知标识符自动生成带有 Functional API 的 Enum 类

    例如:

    known_values = {'a':1,'b':2,'c':1}    # only one of 'a' and 'c' will be used
                                          # 'a' guaranteed in 3.7+, implementation-specific before
                                          # (https://stackoverflow.com/q/39980323)
    extracted_values = (1,2,3,4,5)
    
    known_values_reverse = {}
    for k,v in known_values.items():
        if v not in known_values_reverse:
            known_values_reverse[v]=k
        del k,v    #if placed outside the loop, will error if known_values is empty
    
    for v in extracted_values:
        if v not in known_values_reverse:
            known_values_reverse[v]=str(v)
        del v
    
    AutoFooEnum = enum.Enum(value='AutoFooEnum',names=(k,v for v,k in known_values_reverse.items()))
    

    生成输出时,您需要使用AutoFooEnum(value).name 输出文本 ID(如果有)或数字。

    当然,您将无法在代码中使用 AutoFooEnum.<id> 引用编号成员,因为数字不是有效标识符。但看起来你不需要那个。

    【讨论】:

      【解决方案5】:

      怎么样?

      from enum import Enum
      
      
      class CraftTypes(Enum):
          wood_work = 0
          welding = 1
          mechanics = 2
          unknown = 3
          # 3 and above is unknown
      
      
          @classmethod
          def _missing_(cls, number):
              return cls(cls.unknown)
      

      简单和pythonic...

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-13
        相关资源
        最近更新 更多