18

Is there a better way to create a long list of enumeration with auto-numbering starting at 0? The closest that I can find is:

class Color(Enum):
    red, green, blue=range(3)

However, the above approach requires knowing the total items in the enumeration beforehand.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
cash
  • 181
  • 1
  • 1
  • 3
  • 2
    There's an `AutoNumber` in the docs: https://docs.python.org/3/library/enum.html#autonumber – jonrsharpe Dec 22 '16 at 20:08
  • 1
    What do you gain by starting at zero when the elements are arbitrary? – Steven Rumbalski Dec 22 '16 at 20:09
  • @jonrsharpe `AutoNumber` starts at 1, much like the functional API: `Color = Enum('Color', ['red', 'green', 'blue'])` – AChampion Dec 22 '16 at 20:11
  • 2
    Why do you need it to start at `0`, according to the docs enum members evaluate to `True` so starting at `1` was chosen to avoid confusion with `0` being `False`. – AChampion Dec 22 '16 at 20:14
  • `AutoNumber` isn't defined by the module; it's just an example that you could easily adapt to Python 2 and starting with 0. – chepner Dec 22 '16 at 20:15
  • @AChampion not necessarily – jonrsharpe Dec 22 '16 at 20:16
  • @jonrsharpe understood. – AChampion Dec 22 '16 at 20:17
  • 4
    I tried to use the enumeration as an index to an array which starts at 0, and I don't like to manually assign a value to each item just in case I have to reinsert a new item to the existing enumeration. I have checked the class AutoNumber(Enum) as answered below, however, it seems that you have to use Color.red.value() instead of Color.red – cash Dec 22 '16 at 21:27

7 Answers7

19

As @jonrsharpe already showed, an Enum can be created like this:

Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])

This will be indexed (starting) by 1.

The official documentation states, from Python 3.5:

use the start parameter to specify a different starting value

As the documentation states, you can do exactly that:

Color = Enum('Color', ['RED', 'GREEN', 'BLUE'], start=0)
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Spenhouet
  • 6,556
  • 12
  • 51
  • 76
14

The documentation provides a recipe for autonumbering that can easily be adapted to start from zero:

class AutoNumber(Enum):
     def __new__(cls):
        value = len(cls.__members__)  # note no + 1
        obj = object.__new__(cls)
        obj._value_ = value
        return obj

Then you can create it with arbitrary members:

class Color(AutoNumber):
    red = ()
    green = ()
    blue = ()

Alternatively, note that the functional API can take an iterable of key-value pairs:

from itertools import count

Color = Enum('Color', zip(['red', 'green', 'blue'], count()))

itertools.count is basically an open-ended equivalent of range.


However, the docs also provide the reason for members generally starting with 1:

The reason for defaulting to 1 as the starting number and not 0 is that 0 is False in a boolean sense, but enum members all evaluate to True.

Starting from zero may lead to confusing behaviour later on.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
10

If you prefer declaring a class in an old fashioned way, in Python 3.6.4 you can try something like this:

from enum import Enum, auto

class MyEnum(Enum):
    FIELD_1 = 0
    FIELD_2 = auto()
    FIELD_3 = auto()

print(list(MyEnum))

should result in:

>> [<MyEnum.FIELD_1: 0>, <MyEnum.FIELD_2: 1>, <MyEnum.FIELD_3: 2>]

If you don't set FIELD_1 to 0, then enum property values shall start from 1.

aleksandarbos
  • 496
  • 4
  • 12
9

The OP clarified that their motivation is to use the enum value as an array index, which implies consecutive numbering starting from zero.

The documentation states:

The goal of the default [autonumbering] methods is to provide the next int in sequence with the last int provided, but the way it does this is an implementation detail and may change.

Hence it may be wise to define the autonumbering method explicitly, for example:

from enum import IntEnum, auto
class PrimaryColours(IntEnum):
    def _generate_next_value_(name, start, count, last_values):
        """generate consecutive automatic numbers starting from zero"""
        return count

    RED = auto()
    GREEN = auto()
    BLUE = auto()

This will ensure that the enum values are assigned consequtive values starting from zero:

print(PrimaryColours.RED.value,
      PrimaryColours.GREEN.value,
      PrimaryColours.BLUE.value)

> 0 1 2

Note that the value property can be omitted if the context doesn't require it, e.g.:

orange = (255, 102, 0)
print(orange[PrimaryColours.GREEN])

> 102

Martin CR
  • 1,250
  • 13
  • 25
  • Simplest and most suitable solution IMHO. Although the documentation has a strange parameter name for the 1st parameter of the method. I have updated also by using leading underscore `_xxx` for unused parameters. – Jean-Francois T. Sep 11 '20 at 08:02
  • `_generate_next_value_()` is in fact a static method. The first parameter is not `self` it is the member identifier name. The method works the same way regardless it is or it is not decorated using `@staticmethod` The difference is only when called on an instance which `auto()` does not do. – pabouk - Ukraine stay strong Jun 07 '22 at 21:58
  • 1
    Nice! This is similar to what they do in the [documentation](https://docs.python.org/3/library/enum.html#using-automatic-values), but there they use variable names `name`, `start`, `count`, and `last_values` for the `_generate_next_value_` method. Out of curiosity, why did you choose to prefix the `start` and `last_values` variables with underscores? – HelloGoodbye Jul 01 '22 at 18:41
  • I didn’t - someone else changed it in an edit – Martin CR Jul 02 '22 at 21:27
  • 1
    @MartinCR it would be helpful if you revert the wrong edit by Jean-Francois T. Also I would recommend adding the `@staticmethod` decorator to make it clear that it is a static method. – pabouk - Ukraine stay strong Jul 18 '22 at 10:37
  • @pabouk thanks for the heads up: I didn't know it was possible to revert an edit like that – Martin CR Jul 19 '22 at 16:09
  • @pabouk-Ukrainestaystrong I tried it with the ```@staticmethod``` decorator but enum.py then fails with "staticmethod object is not callable" – Martin CR Jul 19 '22 at 16:35
  • @MartinCR I must say that I did not test it, sorry... but I would not expect the fail. I will test it later. – pabouk - Ukraine stay strong Jul 19 '22 at 16:43
  • 2
    @pabouk-Ukrainestaystrong: It doesn't work with `@staticmethod` because there is code in the `__setitem__` for the customized `dict` that underpins `Enum` that uses `setattr` to attach the raw `_generate_next_value_` (not passed through descriptor protocol) to the `dict`'s instance. When attached to the instance rather than the class, it doesn't invoke the descriptor protocol on lookup, so `self._generate_next_value_` gets back the `staticmethod` object itself, not the unbound function that you'd get from looking it up on an instance of a class with a `staticmethod`. – ShadowRanger Jul 19 '22 at 17:02
  • 1
    Note: The problem with `staticmethod` objects not being callable [was fixed in 3.10](https://github.com/python/cpython/issues/87848) (they now delegate to the function they wrap when called), so it might work for people on 3.10+, but not on 3.9 and below. – ShadowRanger Jul 19 '22 at 17:04
  • @ShadowRanger That figures: I'm on 3.9. – Martin CR Jul 19 '22 at 17:06
  • @MartinCR: Side-note: I suspect the reason the underscores were added was to follow a convention (don't think it's a Python one, I think I've only seen it commonly used in Rust, where [the compiler supports it specially](https://stackoverflow.com/q/55078271/364696)) that underscore prefixes on arguments indicate arguments that must be accepted for whatever reason but not used. That said, it makes sense in Rust because you can't call a function with explicit keyword arguments, in Python, you can, so the convention doesn't make sense unless parameters are forced to be positional-only. – ShadowRanger Jul 19 '22 at 17:11
  • Why is `_generate_next_value_` required here? – Gulzar Nov 10 '22 at 10:25
1

Just do;

from enum import IntEnum

color = IntEnum('color', ['red', 'green', 'blue'], start=0)

Or if you want to have a class;

class Color (IntEnum):
    def __init__(*args, **kwargs):
        start = kwargs.pop('start', None)
        if start is None:
            kwargs['start'] = 0
        super().__init__(*args, **kwargs)
Vinzent
  • 1,070
  • 1
  • 9
  • 14
0

I came across this question and answer because I was looking for a way to set the starting value of a IntEnum with automatic numbering. I found the answer to my question putting together bits and pieces of the answers here.

I am also adding my answer because I think it may be useful to have a general solution.


from enum import IntEnum, auto, unique

class MyEnum(IntEnum):
    
    def _generate_next_value_(name, start, count, last_values):
        # name is the item name
        #
        # start is the automatic numbering starting value and 
        # it is exactly what you want to change. Default = 1
        # Set start to whatever number
        # you want. 0 in the case of the current question.
        
        start=0
        
        #
        # count is the number of items in the enumerator so far
        # 
        # last_values is a list of used values so far.

        return IntEnum._generate_next_value_(name, start, count, last_values)
                
    A = auto()
    B = auto()
    F = auto()
    C = auto()
    D = auto()
    
for e in MyEnum:
    print(f'{e=}') 

The idea is very simple, your enumerator subclass of IntEnum is overloading the base class generate_next_value method setting a different start value. Very simple, extremely elegant and works for all possible start values.

toto
  • 110
  • 1
  • 6
0

Another alternative, I based on the emums.py

from enum import IntEnum

class IntEnumZero(IntEnum):
    # Taken from enums.py
    def _generate_next_value_(name, start, count, last_values):
        for last_value in reversed(last_values):
            try:
                return last_value + 1
            except TypeError:
                pass
        else:
            return 0 #<---CHANGED to 0 instead of <start>


class UsbError(IntEnumZero):
    ZERO = auto() # 0
    ONE = auto() # 1
Alexander Taubenkorb
  • 3,031
  • 2
  • 28
  • 30