Reputation: 515
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
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:
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 aenum
1 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
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