25

I'm having trouble working with an Enum where some attributes have the same value. I think Enums are so new to python that I can't find any other reference to this issue. In any case, let's say I have the following

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

Clearly these are the card numbers and their corresponding values in black jack. The ten through king have the same value. But if I do something like print(CardNumber.QUEEN), I get back <CardNumber.TEN: 10>. What's more, if I iterate over these, it simply iterates over unique values.

>>> 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

How can I get around this issue? I want CardNumber.QUEEN and CardNumber.TEN to be unique, and both appear in any iteration. The only thing I could think of was to give each attribute a second value which would act as a distinct id, but that seems unpythonic.

zephyr
  • 2,182
  • 3
  • 29
  • 51

5 Answers5

23

Update

Using aenum1 you have a couple choices:

  • use NamedConstant instead: does not provide any of the Enum extras (iterating, lookups, etc.) [see: original answer below]

  • use NoAlias: has all the normal Enum behavior except every member is unique and by-value lookups are not available

An example of 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

and in use:

>>> 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

Original Answer

If you want named constants and don't care about the other features of Enums, you can use the NamedConstant class from the aenum library:

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

Duplicate values are still distinct:

>>> CardNumber.TEN is CardNumber.JACK
False

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

>>> CardNumber.TEN == 10
True

1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
17

Yes, labels with duplicate values are turned into aliases for the first such label.

You can enumerate over the __members__ attribute, it is an ordered dictionary with the aliases included:

>>> 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

However, if you must have label-and-value pairs that are unique (and not aliases), then enum.Enum is the wrong approach here; it doesn't match the usecases for a card game.

In that case it'll be better to use a dictionary (consider using collections.OrderedDict() if order is important too).

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Both you and jonrsharpe are correct about the fact that I should be using Dicts for this case. I was merely using this as an example, but you have helped me solve my issue, in any case. – zephyr Jul 21 '15 at 11:15
1

A simple way to do this is to put the value inside an object:

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

By the way, do not use the @dataclass decorator for the Value class. Dataclasses compare the values of its attributes to check if objects are equal, meaning that it would behave exactly like the default Enum!

drakenation
  • 382
  • 3
  • 15
0

Thanks for the answers here but for those who use enum.auto() here is an extended answer for having alias or duplicate enum values

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 :)
Praveen Kulkarni
  • 2,816
  • 1
  • 23
  • 39
  • You actually don't need to define the predict alias outside of the class definition, nor do you actually need to add this indirection. You can set `PREDICT = enum.auto()` and use `INFER = PREDICT` on the next line! – alkasm Jan 29 '21 at 10:51
0

For those still not considering using aenum or OrderedDict, one can still hack around:

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

This rely on the fact that standard enum.EnumMeta.__new__ deduplicates with an O(n²) algorithm to handle unhashable values and should be considered bad practice. It stills achieve the desired effect:

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

Tested with Python3.7.4 and should be portable across cpython standard library.

plcp
  • 1
  • 1