【问题标题】:Python Enums with duplicate values具有重复值的 Python 枚举
【发布时间】:2015-07-21 11:05:26
【问题描述】:

我在使用某些属性具有相同值的枚举时遇到问题。我认为枚举对 python 来说太新了,以至于我找不到任何其他关于这个问题的参考。无论如何,假设我有以下内容

class CardNumber(Enum):
    ACE      = 11
    TWO      = 2
    THREE    = 3
    FOUR     = 4
    FIVE     = 5
    SIX      = 6
    SEVEN    = 7
    EIGHT    = 8
    NINE     = 9
    TEN      = 10
    JACK     = 10
    QUEEN    = 10
    KING     = 10

很明显,这些是黑杰克中的卡号及其对应的值。十通王具有相同的价值。但是,如果我执行print(CardNumber.QUEEN) 之类的操作,我会返回<CardNumber.TEN: 10>。更重要的是,如果我对这些进行迭代,它只会对唯一值进行迭代。

>>> for elem in CardNumber:
...     print(elem)
CardNumber.ACE
CardNumber.TWO
CardNumber.THREE
CardNumber.FOUR
CardNumber.FIVE
CardNumber.SIX
CardNumber.SEVEN
CardNumber.EIGHT
CardNumber.NINE
CardNumber.TEN

我该如何解决这个问题?我希望 CardNumber.QUEEN 和 CardNumber.TEN 是唯一的,并且都出现在任何迭代中。我唯一能想到的就是为每个属性赋予第二个值,该值将充当不同的 id,但这似乎不符合 Python 标准。

【问题讨论】:

  • Enum 显然不是正确的选择。为什么不呢?一个OrderedDict 代替?

标签: python enums


【解决方案1】:

更新

使用aenum1 你有几个选择:

  • 改用NamedConstant:不提供任何Enum 附加功能(迭代、查找等)[请参阅:下面的原始答案]

  • 使用NoAlias:具有所有正常的Enum 行为,除了每个成员都是唯一的并且按值查找不可用

NoAlias 的一个例子:

from aenum import Enum, NoAlias

class CardNumber(Enum):

    _order_ = 'EIGHT NINE TEN JACK QUEEN KING ACE'  # only needed for Python 2.x
    _settings_ = NoAlias

    EIGHT    = 8
    NINE     = 9
    TEN      = 10
    JACK     = 10
    QUEEN    = 10
    KING     = 10
    ACE      = 11

并在使用中:

>>> list(CardNumber)
[<CardNumber.EIGHT: 8>, <CardNumber.NINE: 9>, <CardNumber.TEN: 10>, <CardNumber.JACK: 10>, <CardNumber.QUEEN: 10>, <CardNumber.KING: 10>, <CardNumber.ACE: 11>]

>>> CardNumber.QUEEN == CardNumber.KING
False

>>> CardNumber.QUEEN is CardNumber.KING
False

>>> CardNumber.QUEEN.value == CardNumber.KING.value
True

>>> CardNumber(8)
Traceback (most recent call last):
  ...
TypeError: NoAlias enumerations cannot be looked up by value

原答案

如果你想要命名常量并且不关心Enums 的其他特性,你可以使用aenum library 中的NamedConstant 类:

from aenum import NamedConstant

class CardNumber(NamedConstant):
    ACE      = 11
    TWO      = 2
    THREE    = 3
    FOUR     = 4
    FIVE     = 5
    SIX      = 6
    SEVEN    = 7
    EIGHT    = 8
    NINE     = 9
    TEN      = 10
    JACK     = 10
    QUEEN    = 10
    KING     = 10

重复值仍然是不同的:

>>> CardNumber.TEN is CardNumber.JACK
False

>>> CardNumber.TEN == CardNumber.JACK
True

>>> CardNumber.TEN == 10
True

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

【讨论】:

    【解决方案2】:

    是的,具有重复值的标签将转换为第一个此类标签的别名。

    您可以枚举__members__ 属性,它是一个包含别名的有序字典:

    >>> for name, value in CardNumber.__members__.items():
    ...     print(name, value)
    ... 
    ACE CardNumber.ACE
    TWO CardNumber.TWO
    THREE CardNumber.THREE
    FOUR CardNumber.FOUR
    FIVE CardNumber.FIVE
    SIX CardNumber.SIX
    SEVEN CardNumber.SEVEN
    EIGHT CardNumber.EIGHT
    NINE CardNumber.NINE
    TEN CardNumber.TEN
    JACK CardNumber.TEN
    QUEEN CardNumber.TEN
    KING CardNumber.TEN
    

    但是,如果您必须拥有唯一的标签-值对(而不是别名),那么enum.Enum 在这里是错误的方法;它与纸牌游戏的用例不匹配。

    在这种情况下,最好使用字典(如果顺序也很重要,请考虑使用collections.OrderedDict())。

    【讨论】:

    • 你和 jonrsharpe 都认为我应该在这种情况下使用 Dicts 是正确的。我只是以此为例,但无论如何,你已经帮助我解决了我的问题。
    【解决方案3】:

    一种简单的方法是将值放入对象中:

    class Value:
    
        def __init__(self, value:Any):
            self.value = value
    
    
    class MyEnum(Enum):
    
        item1 = Value(0)
        item2 = Value(0)
        item3 = Value(1)
    
        @property
        def val(self):
            return self.value.value
    
    assert MyEnum.item1.val == 0
    assert MyEnum.item2.val == 0
    assert MyEnum.item3.val == 1
    

    顺便说一句,不要对 Value 类使用 @dataclass 装饰器。数据类比较其属性的值以检查对象是否相等,这意味着它的行为与默认 Enum 完全相同!

    【讨论】:

      【解决方案4】:

      感谢这里的答案,但对于那些使用 enum.auto() 的人来说,这里是具有别名或重复枚举值的扩展答案

      import enum
      PREDICT_ALIAS = enum.auto()
      
      class Mode(enum.Enum):
          FIT = enum.auto()
          EVALUATE = enum.auto()
          PREDICT = PREDICT_ALIAS 
          INFER = PREDICT_ALIAS # alias mode
      
      print(Mode.PREDICT is Mode.INFER)  # return True :)
      

      【讨论】:

      • 您实际上不需要在类定义之外定义 predict 别名,也不需要添加此间接。您可以设置PREDICT = enum.auto(),并在下一行使用INFER = PREDICT
      【解决方案5】:

      对于那些还没有考虑使用aenumOrderedDict 的人来说,仍然可以随意破解:

      import enum
      
      class AliasedEnum(enum.Enum):
          def __init_subclass__(cls, *kargs, **kwargs):
              import inspect
      
              # unfortunately, there's no cleaner ways to retrieve original members
              for stack in reversed(inspect.stack()):
                  frame_locals = stack[0].f_locals
                  enum_members = frame_locals.get('enum_members')
                  if enum_members is None:
                      try:
                          enum_members = frame_locals['classdict']._member_names
                      except (KeyError, AttributeError):
                          continue
                  break
              else:
                  raise RuntimeError('Unable to checkout AliasedEnum members!')
      
              # patch subclass __getattribute__ to evade deduplication checks
              cls._shield_members = list(enum_members)
              cls._shield_getattr = cls.__getattribute__
              def patch(self, key):
                  if key.startswith('_shield_'):
                      return object.__getattribute__(self, key)
                  if key.startswith('_value'):
                      if not hasattr(self, '_name_'):
                          self._shield_counter = 0
                      elif self._shield_counter < self._shield_members.index(self._name_):
                          self._shield_counter += 1
                          class unequal:
                              pass
                          return unequal
                  return self._shield_getattr(key)
              cls.__getattribute__ = patch
      
      class Fck(AliasedEnum):
          A = 0
          B = 0
          C = 0
      

      这依赖于标准 enum.EnumMeta.__new__ 使用 O(n²) 算法进行重复数据删除以处理不可散列值的事实,应该被视为不好的做法。它仍然达到了预期的效果:

      $ python -i aliased_enum.py
      >>> Fck.A
      <Fck.A: 0>
      >>> Fck.B
      <Fck.B: 0>
      >>> Fck.C
      <Fck.C: 0>
      

      使用Python3.7.4 测试,应该可以跨cpython 标准库移植。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-09-19
        • 2012-08-31
        • 1970-01-01
        • 1970-01-01
        • 2016-03-05
        • 2017-03-25
        • 2019-07-18
        • 2011-08-09
        相关资源
        最近更新 更多