【问题标题】:How to extend Python Enum?如何扩展 Python 枚举?
【发布时间】:2016-02-14 06:40:51
【问题描述】:

在 Python 3.4 中扩展 Enum 类型的最佳做法是什么?甚至有可能这样做吗?

例如:

from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(EventStatus):
   duplicate = 2
   unknown = 3

Traceback (most recent call last):
...
TypeError: Cannot extend enumerations

目前没有可能的方法来创建具有成员的基本枚举类并将其用于其他枚举类(如上面的示例中)。有没有其他方法可以实现 Python 枚举的继承?

【问题讨论】:

  • 枚举背后的想法是您拥有该类型所有值的完整列表。如果你扩展它并添加更多值,就会破坏枚举的最基本属性。
  • @user2357112:谢谢,这是我问题的答案。
  • @user2357112 我不明白这一点——上面的代码并没有为EventStatus添加更多的值,它试图创建一个继承EventStatus的值的新类型,并且还有一个再多一点。 Afaics,EventStatus 是清白的。为什么这会破坏基本属性?
  • @Cai:如果允许,新值将是EventStatus 的实例,因为新类将是EventStatus 的子类。 (这与您不能继承 bool 的原因相同。)

标签: python python-3.x enums


【解决方案1】:

仅当枚举未定义任何成员时,才允许对枚举进行子类化。

允许定义成员的枚举子类化会导致违反类型和实例的一些重要不变量。

https://docs.python.org/3/library/enum.html#restricted-enum-subclassing

所以不,这不是直接可能的。

【讨论】:

  • 还有其他内置方法可以做到这一点吗?
  • @falek.marcin:您需要提供更多使用信息。您可以让BookingStatus 直接从Enum 继承并从第一个重复名称值;它有点多余,它们不会互操作(它们是不同的类型),但如果你只使用一组值,那很好。
  • @falek.marcin:可能不会。我认为你能做的最好的事情是使用另一个更简单的 Enum 实现。有plenty of them。
  • @ShadowRanger:是的,这是一个简单的解决方案,但我想避免重复,所以我只是好奇是否有可能扩展枚举。
  • 作为记录,它没有意义的原因是 Enum 类型使用基于身份的比较,并且枚举的值是不可变的。并且值的type 在确定相对排序、成员资格测试等时很重要。当您定义success 时,其type 不变地设置为EventStatus;如果您被允许从EventStatus 继承,那么突然success 也需要成为BookingStatus(这样做会违反不变性和/或导致基于type 的测试出现问题)。
【解决方案2】:

虽然不常见,但有时从多个模块创建枚举很有用。 aenum1 库通过 extend_enum 函数支持此功能:

from aenum import Enum, extend_enum

class Index(Enum):
    DeviceType    = 0x1000
    ErrorRegister = 0x1001

for name, value in (
        ('ControlWord', 0x6040),
        ('StatusWord', 0x6041),
        ('OperationMode', 0x6060),
        ):
    extend_enum(Index, name, value)

assert len(Index) == 5
assert list(Index) == [Index.DeviceType, Index.ErrorRegister, Index.ControlWord, Index.StatusWord, Index.OperationMode]
assert Index.DeviceType.value == 0x1000
assert Index.StatusWord.value == 0x6041

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

【讨论】:

    【解决方案3】:

    直接调用 Enum 类并使用链允许扩展(连接)现有枚举。

    我在使用 CANopen 时遇到了扩展枚举的问题 执行。 0x1000 到 0x2000 范围内的参数索引 对所有 CANopen 节点都是通用的,而例如范围从 0x6000 onwards 取决于 open 节点是否是驱动器、io-module 等。

    nodes.py:

    from enum import IntEnum
    
    class IndexGeneric(IntEnum):
        """ This enum holds the index value of genric object entrys
        """
        DeviceType    = 0x1000
        ErrorRegister = 0x1001
    
    Idx = IndexGeneric
    

    drives.py:

    from itertools import chain
    from enum import IntEnum
    from nodes import IndexGeneric
    
    class IndexDrives(IntEnum):
        """ This enum holds the index value of drive object entrys
        """
        ControlWord   = 0x6040
        StatusWord    = 0x6041
        OperationMode = 0x6060
    
    Idx= IntEnum('Idx', [(i.name, i.value) for i in chain(IndexGeneric,IndexDrives)])
    

    【讨论】:

    • 我想在 MIT 许可的 typhon library 中使用从您的最终 sn-p 派生的代码。你愿意根据 MIT 兼容的许可证重新授权吗?我会保留完整的归属。
    • @gerrit 是的,当然可以随意使用它!抱歉回复晚了
    【解决方案4】:

    我在 3.8 上测试过这种方式。我们可以继承现有的枚举,但我们也需要从基类中继承(在最后一个位置)。

    Docs:

    一个新的 Enum 类必须有一个基础 Enum 类,最多一个具体的 数据类型,以及尽可能多的基于对象的混合类。命令 这些基类是:

    class EnumName([mix-in, ...,] [data-type,] base-enum):
        pass
    

    示例:

    class Cats(Enum):
        SIBERIAN = "siberian"
        SPHINX = "sphinx"
    
    
    class Animals(Cats, Enum):
        LABRADOR = "labrador"
        CORGI = "corgi"
    

    之后,您可以访问 Cats from Animals:

    >>> Animals.SIBERIAN
    <Cats.SIBERIAN: 'siberian'>
    

    但是如果你想遍历这个枚举,只能访问新成员:

    >>> list(Animals)
    [<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
    

    实际上这种方式是为了从基类继承方法,但是你可以将它用于有这些限制的成员。

    另一种方式(有点hacky)

    如上所述,编写一些函数将两个枚举合二为一。我已经写了这个例子:

    def extend_enum(inherited_enum):
        def wrapper(added_enum):
            joined = {}
            for item in inherited_enum:
                joined[item.name] = item.value
            for item in added_enum:
                joined[item.name] = item.value
            return Enum(added_enum.__name__, joined)
        return wrapper
    
    
    class Cats(Enum):
        SIBERIAN = "siberian"
        SPHINX = "sphinx"
    
    
    @extend_enum(Cats)
    class Animals(Enum):
        LABRADOR = "labrador"
        CORGI = "corgi"
    

    但是在这里我们遇到了另一个问题。如果我们想比较成员,它会失败:

    >>> Animals.SIBERIAN == Cats.SIBERIAN
    False
    

    这里我们可能只比较新创建成员的名称和值:

    >>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
    True
    

    但是如果我们需要对新的 Enum 进行迭代,它可以正常工作:

    >>> list(Animals)
    [<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
    

    所以选择你的方式:简单继承,使用装饰器模拟继承(实际上是重新创建),或者添加一个新的依赖项,比如 aenum(我还没有测试过,但我希望它支持我描述的所有功能)。

    【讨论】:

    • 如果您不想与基类值进行比较,则最好。谢谢
    • Python 3.8.6 打破了这一点。正在研究 python 3.8.5
    • 是的,枚举继承中有一个错误。我在3.8.6遇到过,但是在3.8.2和3.9.0都没有重现。
    • @MikhaliBulygin 你错了。以前的版本中有一个错误,他们在 3.8.6 中有意修复了该错误。查看我发布的链接
    • 好答案!事实上,强制从基类继承是绝对有意义的。美丽的蟒蛇!
    【解决方案5】:

    对于正确的类型说明,您可以使用Union 运算符:

    from enum import Enum
    from typing import Union
    
    class EventStatus(Enum):
       success = 0
       failure = 1
    
    class BookingSpecificStatus(Enum):
       duplicate = 2
       unknown = 3
    
    BookingStatus = Union[EventStatus, BookingSpecificStatus]
    
    example_status: BookingStatus
    example_status = BookingSpecificStatus.duplicate
    example_status = EventStatus.success
    

    【讨论】:

    • 值得注意:typing 模块需要 Python 3.5+。
    • 这似乎没有提供任何功能
    【解决方案6】:

    我选择使用元类方法来解决这个问题。

    from enum import EnumMeta
    
    class MetaClsEnumJoin(EnumMeta):
        """
        Metaclass that creates a new `enum.Enum` from multiple existing Enums.
    
        @code
            from enum import Enum
    
            ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
            ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
            class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
                pass
    
            print(ENUMJOINED.a)
            print(ENUMJOINED.b)
            print(ENUMJOINED.c)
            print(ENUMJOINED.d)
        @endcode
        """
    
        @classmethod
        def __prepare__(metacls, name, bases, enums=None, **kargs):
            """
            Generates the class's namespace.
            @param enums Iterable of `enum.Enum` classes to include in the new class.  Conflicts will
                be resolved by overriding existing values defined by Enums earlier in the iterable with
                values defined by Enums later in the iterable.
            """
            #kargs = {"myArg1": 1, "myArg2": 2}
            if enums is None:
                raise ValueError('Class keyword argument `enums` must be defined to use this metaclass.')
            ret = super().__prepare__(name, bases, **kargs)
            for enm in enums:
                for item in enm:
                    ret[item.name] = item.value  #Throws `TypeError` if conflict.
            return ret
    
        def __new__(metacls, name, bases, namespace, **kargs):
            return super().__new__(metacls, name, bases, namespace)
            #DO NOT send "**kargs" to "type.__new__".  It won't catch them and
            #you'll get a "TypeError: type() takes 1 or 3 arguments" exception.
    
        def __init__(cls, name, bases, namespace, **kargs):
            super().__init__(name, bases, namespace)
            #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older.  You'll get a
            #"TypeError: type.__init__() takes no keyword arguments" exception.
    

    这个元类可以这样使用:

    >>> from enum import Enum
    >>>
    >>> ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
    >>> ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
    >>> class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
    ...     e = 5
    ...     f = 6
    ...
    >>> print(repr(ENUMJOINED.a))
    <ENUMJOINED.a: 1>
    >>> print(repr(ENUMJOINED.b))
    <ENUMJOINED.b: 2>
    >>> print(repr(ENUMJOINED.c))
    <ENUMJOINED.c: 3>
    >>> print(repr(ENUMJOINED.d))
    <ENUMJOINED.d: 4>
    >>> print(repr(ENUMJOINED.e))
    <ENUMJOINED.e: 5>
    >>> print(repr(ENUMJOINED.f))
    <ENUMJOINED.f: 6>
    

    此方法使用与源Enums 相同的名称-值对创建一个新的Enum,但生成的Enum 成员仍然是唯一的。名称和值将是相同的,但它们将无法按照 Python 的 Enum 类设计的精神与其起源进行直接比较:

    >>> ENUMA.b.name == ENUMJOINED.b.name
    True
    >>> ENUMA.b.value == ENUMJOINED.b.value
    True
    >>> ENUMA.b == ENUMJOINED.b
    False
    >>> ENUMA.b is ENUMJOINED.b
    False
    >>>
    

    注意在命名空间冲突时会发生什么:

    >>> ENUMC = Enum('ENUMA', {'a': 1, 'b': 2})
    >>> ENUMD = Enum('ENUMB', {'a': 3})
    >>> class ENUMJOINEDCONFLICT(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMC, ENUMD)):
    ...     pass
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 19, in __prepare__
      File "C:\Users\jcrwfrd\AppData\Local\Programs\Python\Python37\lib\enum.py", line 100, in __setitem__
        raise TypeError('Attempted to reuse key: %r' % key)
    TypeError: Attempted to reuse key: 'a'
    >>>
    

    这是由于基本 enum.EnumMeta.__prepare__ 返回一个特殊的 enum._EnumDict 而不是典型的 dict 对象,该对象在键分配时表现不同。您可能希望通过用try-except TypeError 将其包围来抑制此错误消息,或者可能有一种方法可以在调用super().__prepare__(...) 之前修改命名空间。

    【讨论】:

      【解决方案7】:

      我认为你可以这样做:

      import enum
      from typing import List
      from enum import Enum
      
      def extend_enum(current_enum, names: List[str], values: List = None):
          if not values:
              values = names
      
          for item in current_enum:
              names.append(item.name)
              values.append(item.value)
      
          return enum.Enum(current_enum.__name__, dict(zip(names, values)))
      
      class EventStatus(Enum):
         success = 0
         failure = 1
      
      class BookingStatus(object):
         duplicate = 2
         unknown = 3
      
      BookingStatus = extend_enum(EventStatus, ['duplicate','unknown'],[2,3])
      

      重点是:

      • python 可以在运行时改变任何东西
      • 类也是对象

      【讨论】:

        【解决方案8】:

        另一种方式:

        Letter = Enum(value="Letter", names={"A": 0, "B": 1})
        LetterExtended = Enum(value="Letter", names=dict({"C": 2, "D": 3}, **{i.name: i.value for i in Letter}))
        

        或者:

        LetterDict = {"A": 0, "B": 1}
        Letter = Enum(value="Letter", names=LetterDict)
        
        LetterExtendedDict = dict({"C": 2, "D": 3}, **LetterDict)
        LetterExtended = Enum(value="Letter", names=LetterExtendedDict)
        

        输出:

        >>> Letter.A
        <Letter.A: 0>
        >>> Letter.C
        Traceback (most recent call last):
          File "<input>", line 1, in <module>
          File "D:\jhpx\AppData\Local\Programs\Python\Python36\lib\enum.py", line 324, in __getattr__
            raise AttributeError(name) from None
        AttributeError: C
        >>> LetterExtended.A
        <Letter.A: 0>
        >>> LetterExtended.C
        <Letter.C: 2>
        

        【讨论】:

          【解决方案9】:

          您不能扩展枚举,但可以通过合并它们来创建一个新枚举。
          Tested for Python 3.6

          from enum import Enum
          
          
          class DummyEnum(Enum):
              a = 1
          
          
          class AnotherDummyEnum(Enum):
              b = 2
          
          
          def merge_enums(class_name: str, enum1, enum2, result_type=Enum):
              if not (issubclass(enum1, Enum) and issubclass(enum2, Enum)):
                  raise TypeError(
                      f'{enum1} and {enum2} must be derived from Enum class'
                  )
          
              attrs = {attr.name: attr.value for attr in set(chain(enum1, enum2))}
              return result_type(class_name, attrs, module=__name__)
          
          
          result_enum = merge_enums(
              class_name='DummyResultEnum',
              enum1=DummyEnum,
              enum2=AnotherDummyEnum,
          )
          

          【讨论】:

            【解决方案10】:

            是的,您可以修改Enum。下面的示例代码有点 hacky,它显然依赖于 Enum 的内部结构,它没有任何业务可以依赖。另一方面,它有效。

            class ExtIntEnum(IntEnum):
                @classmethod
                def _add(cls, value, name):
                    obj = int.__new__(cls, value)
                    obj._value_ = value
                    obj._name_ = name  
                    obj.__objclass__ = cls
            
                    cls._member_map_[name] = obj
                    cls._value2member_map_[value] = obj
                    cls._member_names_.append(name)    
            
            class Fubar(ExtIntEnum):
                foo = 1
                bar = 2
            
            Fubar._add(3,"baz")
            Fubar._add(4,"quux")
            

            具体来说,观察obj = int.__new__() 行。 enum 模块跳过了几个环节来为应该枚举的类找到正确的 __new__ 方法。我们在这里忽略这些圈,因为我们已经知道整数(或者更确切地说,int 的子类的实例)是如何创建的。

            最好不要在生产代码中使用它。如果必须,您真的应该添加防止重复值或名称的保护措施。

            【讨论】:

              【解决方案11】:

              我想从 Django 的 IntegerChoices 继承,由于“无法扩展枚举”的限制,这是不可能的。我认为这可以通过一个相对简单的元类来完成。

              CustomMetaEnum.py:

              class CustomMetaEnum(type):
                  def __new__(self, name, bases, namespace):
                      # Create empty dict to hold constants (ex. A = 1)
                      fields = {}
              
                      # Copy constants from the namespace to the fields dict.
                      fields = {key:value for key, value in namespace.items() if isinstance(value, int)}
                  
                      # In case we're about to create a subclass, copy all constants from the base classes' _fields.
                      for base in bases:
                          fields.update(base._fields)
              
                      # Save constants as _fields in the new class' namespace.
                      namespace['_fields'] = fields
                      return super().__new__(self, name, bases, namespace)
              
                  # The choices property is often used in Django.
                  # If other methods such as values(), labels() etc. are needed
                  # they can be implemented below (for inspiration [Django IntegerChoice source][1])
                  @property
                  def choices(self):
                      return [(value,key) for key,value in self._fields.items()]
              

              main.py:

              from CustomMetaEnum import CustomMetaEnum
              
              class States(metaclass=CustomMetaEnum):
                  A = 1
                  B = 2
                  C = 3
              
              print("States: ")
              print(States.A)
              print(States.B)
              print(States.C)
              print(States.choices)
              
              
              print("MoreStates: ")
              class MoreStates(States):
                  D = 22
                  pass
              
              print(MoreStates.A)
              print(MoreStates.B)
              print(MoreStates.C)
              print(MoreStates.D)
              print(MoreStates.choices)
              

              python3.8main.py:

              States: 
              1
              2
              3
              [(1, 'A'), (2, 'B'), (3, 'C')]
              MoreStates: 
              1
              2
              3
              22
              [(22, 'D'), (1, 'A'), (2, 'B'), (3, 'C')]
              

              【讨论】:

                【解决方案12】:

                这里已经有很多很好的答案,但这里有另一个纯粹使用Enum's Functional API。

                可能不是最漂亮的解决方案,但它避免了代码重复,开箱即用,不需要额外的包/库,它应该足以涵盖大多数用例:

                from enum import Enum
                
                class EventStatus(Enum):
                   success = 0
                   failure = 1
                
                BookingStatus = Enum(
                    "BookingStatus",
                    [es.name for es in EventStatus] + ["duplicate", "unknown"],
                    start=0,
                )
                
                for bs in BookingStatus:
                    print(bs.name, bs.value)
                
                # success 0
                # failure 1
                # duplicate 2
                # unknown 3
                

                如果您想明确分配的值,您可以使用:

                BookingStatus = Enum(
                    "BookingStatus",
                    [(es.name, es.value) for es in EventStatus] + [("duplicate", 6), ("unknown", 7)],
                )
                
                for bs in BookingStatus:
                    print(bs.name, bs.value)
                
                # success 0
                # failure 1
                # duplicate 6
                # unknown 7
                

                【讨论】:

                  猜你喜欢
                  • 2023-04-10
                  • 2020-08-10
                  • 2012-07-09
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多