Robert
Robert

Reputation: 515

Default or invalid value for enumerator

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?

Upvotes: 2

Views: 4871

Answers (2)

Ethan Furman
Ethan Furman

Reputation: 69021

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.

Upvotes: 1

JRodge01
JRodge01

Reputation: 280

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?

Upvotes: 0

Related Questions