python enums with attributes

Question:

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 */
}
Asked By: bmargulies

||

Answers:

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.

Answered By: Martijn Pieters

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.

Answered By: Ethan Furman

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
Answered By: Ovidiu S.

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.

Answered By: leafmeal

Inspired by some of the other answers, I found a way of including additional fields to an enum as ‘transparently’ as possible, overcoming some shortcomings of the other approaches. Everything works the same as if the additional fields weren’t there.

The enum is immutable just like a tuple, the value of the enum is just as it would be without the additional fields, it works just like a normal enum with auto(), and selecting an enum by value works.

import enum

# Common base class for all enums you want to create with additional fields (you only need this once)
class EnumFI(enum.Enum):

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._values = []

    def __new__(cls, *args, **kwargs):
        value = args[0]
        if isinstance(value, enum.auto):
            if value.value == enum._auto_null:
                value.value = cls._generate_next_value_(None, 1, len(cls.__members__), cls._values[:])  # Note: This just passes None for the key, which is generally okay
            value = value.value
            args = (value,) + args[1:]
        cls._values.append(value)
        instance = cls._member_type_.__new__(cls, *args, **kwargs)
        instance._value_ = value
        return instance

    def __format__(self, format_spec):
        return str.__format__(str(self), format_spec)

Then anywhere in the code you can just do:

from enum import auto
from collections import namedtuple

class Color(namedtuple('ColorTuple', 'id r g b'), EnumFI):
    GREEN = auto(), 0, 255, 0
    BLUE = auto(), 0, 0, 255

Example output:

In[4]: Color.GREEN
Out[4]: <Color.GREEN: 1>

In[5]: Color.GREEN.value
Out[5]: 1

In[6]: Color.GREEN.r
Out[6]: 0

In[7]: Color.GREEN.g
Out[7]: 255

In[8]: Color.GREEN.b
Out[8]: 0

In[9]: Color.GREEN.r = 8
Traceback (most recent call last):
  File "/home/phil/anaconda3/envs/dl/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3326, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-9-914a059d9d3b>", line 1, in <module>
    Color.GREEN.r = 8
AttributeError: can't set attribute

In[10]: Color(2)
Out[10]: <Color.BLUE: 2>

In[11]: Color['BLUE']
Out[11]: <Color.BLUE: 2>
Answered By: pallgeuer

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
Answered By: Wolfgang Fahl

For keyword-based initialization of attributes, you might try data-enum, a more lightweight implementation of enum with cleaner syntax for some cases, including this one.

from data_enum import DataEnum

class Item(DataEnum):
    data_attribute_names = ('a', 'b')

Item.GREEN = Item(a='a', b='b')
Item.BLUE = Item(a='c', b='d')

I should note that I am the author of data-enum, and built it specifically to address this use case.

Answered By: Chase Finch

After searching a lot, I found these two working examples!

That’s it my friends!

Codes…

from enum import Enum


class StatusInt(int, Enum):
    READY = (0, "Ready to go!")
    ERROR = (1, "Something wrong!")

    def __new__(cls, value, description):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj._description_ = description
        return obj

    @property
    def description(self):
        return self._description_


class StatusObj(Enum):
    READY = (0, "Ready to go!")
    ERROR = (1, "Something wrong!")

    def __init__(self, value, description):
        self._value_ = value
        self._description_ = description

    @property
    def description(self):
        return self._description_


print(str(StatusInt.READY == StatusInt.ERROR))
print(str(StatusInt.READY.value))
print(StatusInt.READY.description)

print(str(StatusObj.READY == StatusObj.ERROR))
print(str(StatusObj.READY.value))
print(StatusObj.READY.description)

Outputs…

False
0
Ready to go!
False
0
Ready to go!

[Ref(s).: https://docs.python.org/3/library/enum.html#when-to-use-new-vs-init , https://docs.python.org/3/library/enum.html#planet ]

Answered By: Eduardo Lucio

enum-properties provides an extension of the Enum base class that allows attributes on enum values and also allows symmetric mapping backwards from attribute values to their enumeration values.

Add properties to Python enumeration values with a simple declarative syntax. Enum Properties is a lightweight extension to Python’s Enum class. Example:

from enum_properties import EnumProperties, p
from enum import auto

class Color(EnumProperties, p('rgb'), p('hex')):

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

# the named p() values in the Enum's inheritance become properties on
# each value, matching the order in which they are specified

Color.RED.rgb   == (1, 0, 0)
Color.GREEN.rgb == (0, 1, 0)
Color.BLUE.rgb  == (0, 0, 1)

Color.RED.hex   == 'ff0000'
Color.GREEN.hex == '00ff00'
Color.BLUE.hex  == '0000ff'

Properties may also be symmetrically mapped to enumeration values, using s() values:

from enum_properties import EnumProperties, s
from enum import auto

class Color(EnumProperties, s('rgb'), s('hex', case_fold=True)):

    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

# any named s() values in the Enum's inheritance become properties on
# each value, and the enumeration value may be instantiated from the
# property's value

Color((1, 0, 0)) == Color.RED
Color((0, 1, 0)) == Color.GREEN
Color((0, 0, 1)) == Color.BLUE

Color('ff0000') == Color.RED
Color('FF0000') == Color.RED  # case_fold makes mapping case insensitive
Color('00ff00') == Color.GREEN
Color('00FF00') == Color.GREEN
Color('0000ff') == Color.BLUE
Color('0000FF') == Color.BLUE

Color.RED.hex == 'ff0000'
Answered By: bckohan
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.