5

I don't understand why this Enum doesn't have all the members I defined, when I assign a dict as each member's value:

from enum import Enum

class Token(Enum):
    facebook = {
    'access_period': 0,
    'plan_name': ''}

    instagram = {
    'access_period': 0,
    'plan_name': ''}

    twitter = {
    'access_period': 0,
    'plan_name': ''}

if __name__ == "__main__":
    print(list(Token))

The output is:

[<Token.twitter: {'plan_name': '', 'access_period': 0}>]

… but I expected something like:

[<Token.facebook:  {'plan_name': '', 'access_period': 0}>,
 <Token.instagram: {'plan_name': '', 'access_period': 0}>,
 <Token.twitter:   {'plan_name': '', 'access_period': 0}>]

Why aren't all the members shown?

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
Fartash
  • 558
  • 4
  • 12
  • It's because they all have the same value. That's not how enums are supposed to work. – Aran-Fey Apr 20 '17 at 14:51
  • I'm not sure why this question has received a down-vote. The writing could be improved -- yes. However the question shows a not necessarily intuitive behavior of the Enum module. – Michael Hoff Apr 20 '17 at 15:07
  • @Fartash you might want to double-check your spelling and rewrite the question title. – Michael Hoff Apr 20 '17 at 15:10
  • @MichaelHoff: Agreed. The question itself is well worded, shows the code to reproduce the problem, and describes the desired outcome. I wish all questions were this good! – Ethan Furman Apr 20 '17 at 15:14
  • Any reason you need Enum? – Karoly Horvath Apr 20 '17 at 20:55
  • This use case is likely a really bad fit for an `Enum`. Have you considered a plain `dict` instead? If you think you must use an `Enum` at all costs, please explain why. – blubberdiblub Apr 29 '17 at 09:34

2 Answers2

5

Enum enforces unique values for the members. Member definitions with the same value as other definitions will be treated as aliases.

Demonstration:

Token.__members__
# OrderedDict([('twitter',
#               <Token.twitter: {'plan_name': '', 'access_period': 0}>),
#              ('facebook',
#               <Token.twitter: {'plan_name': '', 'access_period': 0}>),
#              ('instagram',
#               <Token.twitter: {'plan_name': '', 'access_period': 0}>)])

assert Token.instagram == Token.twitter

The defined names do all exist, however they are all mapped to the same member.

Have a look at the source code if you are interested:

# [...]
# If another member with the same value was already defined, the
# new member becomes an alias to the existing one.
for name, canonical_member in enum_class._member_map_.items():
    if canonical_member._value_ == enum_member._value_:
        enum_member = canonical_member
        break
else:
    # Aliases don't appear in member names (only in __members__).
    enum_class._member_names_.append(member_name)
# performance boost for any member that would not shadow
# a DynamicClassAttribute
if member_name not in base_attributes:
    setattr(enum_class, member_name, enum_member)
# now add to _member_map_
enum_class._member_map_[member_name] = enum_member
try:
    # This may fail if value is not hashable. We can't add the value
    # to the map, and by-value lookups for this value will be
    # linear.
    enum_class._value2member_map_[value] = enum_member
except TypeError:
    pass
# [...]

Further, it seems to me that you want to exploit the Enum class to modify the value (the dictionary) during run-time. This is strongly discouraged and also very unintuitive for other people reading/using your code. An enum is expected to be made of constants.

Michael Hoff
  • 6,119
  • 1
  • 14
  • 38
  • 1
    The line "The members do exist" could be worded more clearly, maybe "The different names exist, but they all map to the same member" ? – Ethan Furman Apr 20 '17 at 15:45
  • Fair point. I was not too sure about the correct terminology. As `len(Token.__members__) == 3` one could understand that there are three "members". Answer updated. – Michael Hoff Apr 20 '17 at 15:49
  • Hah, I just had to make the same change to my answer! :/ – Ethan Furman Apr 20 '17 at 15:57
4

As @MichaelHoff noted, the behavior of Enum is to consider names with the same values to be aliases1.

You can get around this by using the Advanced Enum2 library:

from aenum import Enum, NoAlias

class Token(Enum):
    _settings_ = NoAlias
    facebook = {
        'access_period': 0,
        'plan_name': '',
        }

    instagram = {
        'access_period': 0,
        'plan_name': '',
        }

    twitter = {
        'access_period': 0,
        'plan_name': '',
        }

if __name__ == "__main__":
    print list(Token)

Output is now:

[
  <Token.twitter: {'plan_name': '', 'access_period': 0}>,
  <Token.facebook: {'plan_name': '', 'access_period': 0}>,
  <Token.instagram: {'plan_name': '', 'access_period': 0}>,
  ]

To reinforce what Michael said: Enum members are meant to be constants -- you shouldn't use non-constant values unless you really know what you are doing.


A better example of using 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

1 See this answer for the standard Enum usage.

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

Community
  • 1
  • 1
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Is there a way to achieve the `NoAlias` behavior for the stdlib? – Michael Hoff Apr 20 '17 at 15:16
  • @MichaelHoff: Not easily, no. It falls into the category of "you can if you really want to, but nobody is going to enjoy reading that code!" ;) – Ethan Furman Apr 20 '17 at 15:19
  • How dislikable is `def __init__(self, *args): self._value_ = (self.name,) + args`? ;) – Michael Hoff Apr 21 '17 at 12:36
  • @MichaelHoff: That will not work with mixin types, such as `int`, and would also make by-value lookups fail (instead of `CardNumber(8)` it would be `CardNumber(('EIGHT', 8))`. – Ethan Furman Apr 21 '17 at 13:38
  • Surprisingly it does work* with `IntEnum` but yes, the lookup via constructor will fail. (*at least the use cases I tested have been just fine) – Michael Hoff Apr 21 '17 at 13:51
  • @MichaelHoff, Huh, interesting. Guess I should have tested that statement first! But hey, I was right on the lookup! :) Plus it's still early here... ;) – Ethan Furman Apr 21 '17 at 13:52