python enums with attributes

24,088

Solution 1

Python 3.4 has a new Enum data type (which has been backported as enum34 and enhanced as aenum1). Both enum34 and aenum2 easily support your use case:

  • aenum (Python 2/3)

      import aenum
      class EnumWithAttrs(aenum.AutoNumberEnum):
          _init_ = 'a b'
          GREEN = 'a', 'b'
          BLUE = 'c', 'd'
    
  • enum34 (Python 2/3) or standard library enum (Python 3.4+)

      import enum
      class EnumWithAttrs(enum.Enum):
    
          def __new__(cls, *args, **kwds):
              value = len(cls.__members__) + 1
              obj = object.__new__(cls)
              obj._value_ = value
              return obj
          def __init__(self, a, b):
              self.a = a
              self.b = b
    
          GREEN = 'a', 'b'
          BLUE = 'c', 'd'
    

And in use:

>>> EnumWithAttrs.BLUE
<EnumWithAttrs.BLUE: 1>

>>> EnumWithAttrs.BLUE.a
'c'

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

2 aenum also supports NamedConstants and metaclass-based NamedTuples.

Solution 2

For Python 3:

class Status(Enum):
    READY = "ready", "I'm ready to do whatever is needed"
    ERROR = "error", "Something went wrong here"

    def __new__(cls, *args, **kwds):
        obj = object.__new__(cls)
        obj._value_ = args[0]
        return obj

    # ignore the first param since it's already set by __new__
    def __init__(self, _: str, description: str = None):
        self._description_ = description

    def __str__(self):
        return self.value

    # this makes sure that the description is read-only
    @property
    def description(self):
        return self._description_

And you can use it as a standard enum or factory by type:

print(Status.READY)
# ready
print(Status.READY.description)
# I'm ready to do whatever is needed
print(Status("ready")) # this does not create a new object
# ready

Solution 3

Before Python 3.4 and the addition of the excellent enum module, a good choice would have been to use a namedtuple:

from collections import namedtuple

Item = namedtuple('abitem', ['a', 'b'])

class Items:
    GREEN = Item('a', 'b')
    BLUE = Item('c', 'd')

These days, any supported version of Python has enum, so please use that module. It gives you a lot more control over how each enum value is produced.

If you give each item a tuple of values, then these are passed to the __init__ method as separate (positional) arguments, which lets you set additional attributes on the enum value:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __init__(self, a, b):
        self.a = a
        self.b = b

This produces enum entries whose value is the tuple assigned to each name, as well as two attributes a and b:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items(('a', 'b'))
<Items.GREEN: ('a', 'b')>

Note that you can look up each enum value by passing in the same tuple again.

If the first item should represent the value of each enum entry, use a __new__ method to set _value_:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __new__(cls, a, b):
        entry = object.__new__(cls) 
        entry.a = entry._value_ = a  # set the value, and the extra attribute
        entry.b = b
        return entry

    def __repr__(self):
        return f'<{type(self).__name__}.{self.name}: ({self.a!r}, {self.b!r})>'

I added a custom __repr__ as well, the default only includes self._value_. Now the value of each entry is defined by the first item in the tuple, and can be used to look up the enum entry:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items('a')
<Items.GREEN: ('a', 'b')>

See the section on __init__ vs. __new__ in the documentation for further options.

Solution 4

Here's another approach which I think is simpler than the others, but allows the most flexibility:

from collections import namedtuple
from enum import Enum

class Status(namedtuple('Status', 'name description'), Enum):
    READY = 'ready', 'I am ready to do whatever is needed'
    ERROR = 'error', 'Something went wrong here'

    def __str__(self) -> str:
        return self.name

It works as expected:

>>> str(Status.READY)
ready

>>> Status.READY
<Status.READY: Status(name='ready', description='I am ready to do whatever is needed')>

>>> Status.READY.description
'I am ready to do whatever is needed'

>>> Status.READY.value
Status(name='ready', description='I am ready to do whatever is needed')

Also you are able to retrieve the enum by name (Thanks @leoll2 for pointing this out). For example

>>> Status['READY']
<Status.READY: Status(name='ready', description='I am ready to do whatever is needed')>

You get the best of namedtuple and Enum.

Solution 5

for small enums @property might work:

class WikiCfpEntry(Enum):
    '''
    possible supported storage modes
    '''
    EVENT = "Event"      
    SERIES = "Series"
    
    @property
    def urlPrefix(self):
        baseUrl="http://www.wikicfp.com/cfp"
        if self==WikiCfpEntry.EVENT:
            url= f"{baseUrl}/servlet/event.showcfp?eventid="
        elif self==WikiCfpEntry.SERIES:
            url= f"{baseUrl}/program?id="
        return url
Share:
24,088
Artem
Author by

Artem

Updated on July 09, 2022

Comments

  • Artem
    Artem almost 2 years

    Consider:

    class Item:
       def __init__(self, a, b):
           self.a = a
           self.b = b
    
    class Items:
        GREEN = Item('a', 'b')
        BLUE = Item('c', 'd')
    

    Is there a way to adapt the ideas for simple enums to this case? (see this question) Ideally, as in Java, I would like to cram it all into one class.

    Java model:

    enum EnumWithAttrs {
        GREEN("a", "b"),
        BLUE("c", "d");
    
        EnumWithAttrs(String a, String b) {
          this.a = a;
          this.b = b;
        }
    
        private String a;
        private String b;
    
        /* accessors and other java noise */
    }
    
  • Lukas Graf
    Lukas Graf over 11 years
    @bmargulies Or some watching. In this video recording of a PyCon talk] Raymond Hettinger explains why named tuples are awesome, how they work and use cases for them. Fun with Python's Newer Tools [11:35 - 26:00]. The examples he demonstrates include an enum type for colors.
  • Ethan Furman
    Ethan Furman over 4 years
    @andilabs: Sounds like you asking for something like mypy. mypy currently supports the stdlib Enum only. Someone could take the Enum support from mypy and translate that for aenum.Enum, but I have not had time.
  • Nairum
    Nairum over 3 years
    Why EnumWithAttrs.BLUE.a returns "c" instead of "a"? I need to get the second value from tuple.
  • Nairum
    Nairum over 3 years
    This is more descriptive and easy to use than the accepted answer.
  • Ethan Furman
    Ethan Furman over 3 years
    @AlexeiMarinichenko: In the example code above, a is the first attribute and b is the second; so to access the second attribute of BLUE you would do EnumWithAttrs.BLUE.b.
  • off99555
    off99555 over 3 years
    It doesn't allow me to select the Status by name e.g. Status('ready'). It's needed when you want user to choose the status from a list of names, and they will type a name e.g. 'ready'
  • off99555
    off99555 over 3 years
    Doesn't allow me to chose an item from its name e.g. I expect Items('a') to return Items.GREEN
  • Martijn Pieters
    Martijn Pieters over 3 years
    @off99555: this answer was posted long before the enum module was added, which is what you really want to use now.
  • Martijn Pieters
    Martijn Pieters over 3 years
    @off99555: I've updated the answer to make use of enum now that that's a universally available option. Note that an Enum won't directly let you map arbitrary items from the tuple of options to an enum value; Items('b') or Items('d') still won't work, only the enum _value_ attribute is supported in lookups. You'd have to define a class method yourself that encodes custom lookup rules, e.g. @classmethod, def lookup(cls, value):, for entry in cls.__members__.values(): if value in {entry.a, entry.b}: return entry.
  • Philip Couling
    Philip Couling almost 3 years
    It'd be nice to have an explanation of why __new__ is required in this context since so may other classes don't require it.
  • Gavin S. Yancey
    Gavin S. Yancey over 2 years
    What does the __new__ do in your second example? Why is it needed?
  • Ethan Furman
    Ethan Furman over 2 years
    @Gavin: it creates the enum member, and it's needed so that the .value attribute is 1, 2, etc., and to assign the member attributes a and b to the passed in values.
  • Eduardo Lucio
    Eduardo Lucio about 2 years
    What if we need something like class Status(str, Enum):? I needed to change the line obj = object.__new__(cls) to obj = str.__new__(cls), but now we have that print(str(Status.READY == Status.ERROR)) results in " True". Any idea? 🤔🤔🤔
  • Eduardo Lucio
    Eduardo Lucio about 2 years
    The def __str__(self): method is unnecessary. 😉
  • Ovidiu S.
    Ovidiu S. about 2 years
    In my humble view, extending str and enum at the same time is wrong. However, if you still want to do it you probably need to overload def __eq__(self, other): as the default str implementation differs from the enum class.
  • leoll2
    leoll2 about 2 years
    @off99555 You can do it with Status["READY"]