Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Enums with duplicate values

Tags:

python

enums

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.

like image 464
zephyr Avatar asked Jul 21 '15 11:07

zephyr


3 Answers

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

like image 127
Martijn Pieters Avatar answered Oct 19 '22 21:10

Martijn Pieters


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.

like image 21
Ethan Furman Avatar answered Oct 19 '22 21:10

Ethan Furman


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!

like image 1
drakenation Avatar answered Oct 19 '22 19:10

drakenation