1

In Python, I've been creating enums using the enum module. Usually with the int-version to allow conversion:

from enum import IntEnum
class Level(IntEnum):
    DEFAULTS    = 0
    PROJECT     = 1
    MASTER      = 2
    COLLECT     = 3
    OBJECT      = 4

I would like to provide some type of invalid or undefined value for variables of this type. For example, if an object is initialized and its level is currently unknown, I would like to create it by doing something like self.level = Level.UNKNOWN or perhaps Level.INVALID or Level.NONE. I usually set the internal value of these special values to -1.

The type of problems I keep running into is that adding any special values like this will break iteration and len() calls. Such as if I wanted to generate some list to hold each level type list = [x] * len(Level), it would add these extra values to the list length, unless I manually subtract 1 from it. Or if I iterated the level types for lvl in Level:, I would have to manually skip over these special values.

So I'm wondering if there is any clever way to fix this problem? Is it pointless to even create an invalid value like this in Python? Should I just be using something like the global None instead? Or is there some way to define the invalid representation of the enumerator so that it doesn't get included in iteration or length logic?

Robert
  • 413
  • 4
  • 12
  • 2
    Wait, *why* are you using `IntEnum`? You really shouldn't unless you want to maintain backwards compatibilities with old code written *before* the enum module was available. Did you read the warning in the docs? – juanpa.arrivillaga Dec 02 '19 at 18:25
  • I'm not very experienced with Python, so its all new to me. What should I be using? – Robert Dec 03 '19 at 13:51

2 Answers2

1

The answer to this problem is similar to the one for Adding NONE and ALL to Flag Enums (feel free to look there for an in-depth explanation; NB: that answer uses a class-type decorator, while the below is a function-type decorator).

def add_invalid(enumeration):
    """
    add INVALID psuedo-member to enumeration with value of -1
    """
    #
    member = int.__new__(enumeration, -1)
    member._name_ = 'INVALID'
    member._value_ = -1
    enumeration._member_map_['INVALID'] = member
    enumeration._value2member_map_[-1] = member
    return enumeration

Which would look like

@add_invalid
class Level(IntEnum):
    DEFAULTS    = 0
    PROJECT     = 1
    MASTER      = 2
    COLLECT     = 3
    OBJECT      = 4

and in use:

>>> list(Level)
[<Level.DEFAULTS: 0>, <Level.PROJECT: 1>, <Level.MASTER: 2>, <Level.COLLECT: 3>, <Level.OBJECT: 4>]

>>> type(Level.INVALID)
<enum 'Level'>

>>> Level.INVALID
<Level.INVALID: -1>

>>> Level(-1)
<Level.INVALID: -1>

>>> Level['INVALID']
<Level.INVALID: -1>

There are a couple caveats to this method:

  • it is using internal enum structures that may change in the future
  • INVALID, while not showing up normally, is otherwise an Enum member (so cannot be changed, deleted, etc.)

If you don't want to use internal structures, and/or you don't need INVALID to actually be an Enum member, you can instead use the Constant class found here:

class Constant:
    def __init__(self, value):
        self.value = value
    def __get__(self, *args):
        return self.value
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.value)

Which would look like

class Level(IntEnum):
    #
    DEFAULTS    = 0
    PROJECT     = 1
    MASTER      = 2
    COLLECT     = 3
    OBJECT      = 4
    #
    INVALID = Constant(-1)

and in use:

>>> Level.INVALID
-1

>>> type(Level.INVALID)
<class 'int'>

>>> list(Level)
[<Level.DEFAULTS: 0>, <Level.PROJECT: 1>, <Level.MASTER: 2>, <Level.COLLECT: 3>, <Level.OBJECT: 4>]

The downside to using a custom descriptor is that it can be changed on the class; you can get around that by using aenum1 and its built-in constant class (NB: lower-case):

from aenum import IntEnum, constant

class Level(IntEnum):
    #
    DEFAULTS    = 0
    PROJECT     = 1
    MASTER      = 2
    COLLECT     = 3
    OBJECT      = 4
    #
    INVALID = constant(-1)

and in use:

>>> Level.INVALID
-1

>>> Level.INVALID = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ethan/.local/lib/python3.6/site-packages/aenum/__init__.py", line 2128, in __setattr__
    '%s: cannot rebind constant %r' % (cls.__name__, name),
AttributeError: Level: cannot rebind constant 'INVALID'

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

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Three answers in one. Thank you for all of the information. I'm going to look at both of these, but I think I like the second (and third). Can you help me understand what you meant by "*it can be changed on the class*"? Did you mean its not constant? – Robert Dec 03 '19 at 14:04
  • @Robert: Exactly. Under normal conditions, that last assignment (*Level.INVALID = ...*) would succeed, but the `aenum.Enum` metaclass blocks it. – Ethan Furman Dec 03 '19 at 15:59
0

Idiomatically speaking, when you use an enumerator it is because you know without a doubt everything will fall into one of the enumerated categories. Having a catch-all "other" or "none" category is common.

If the level of an item isn't known at the time of creation, then you can instantiate all objects with the "unknown" level unless you supply it another level.

Is there a particular reason you are treating these internally with a -1 value? Are these levels erroneous, or are they having an "unknown" level valid?

JRodge01
  • 280
  • 1
  • 7
  • It is used in a state object that links to variable level items. So it is valid for it to be `unknown` while *unlinked*, but invalid while *linked* to an item. However, there is data outside of the enum that relates to the values, and there is no valid data for `unknown`. I could add some if it made sense, but it would never be used (intentionally). – Robert Dec 03 '19 at 13:57