A fast Enum implementation for Python
The purpose is to replace Python3.4+ standard library Enum.
The main objective to using standard library's Enum is that it's super slow.
Features implemented:
- as in stdlib's enums all Enum members are instances of the Enum itself
type(LightEnum.ONE)
# <class 'LightEnum'>
- all enum members have at least
name
andvalue
properties; thename
property is a string containing the class member's name itself; thevalue
property contains the value assigned
class ValuesGivenEnum(metaclass=FastEnum):
ONE: 'ValuesGivenEnum' = 1
FOUR: 'ValuesGivenEnum' = 4
ELEVEN: 'ValuesGivenEnum' = 11
ValuesGivenEnum.FOUR
# <ValuesGivenEnum.FOUR: 4>
ValuesGivenEnum.FOUR.value
# 4
ValuesGivenEnum.FOUR.name
# 'FOUR'
- a lightweight form of enum declaration is possible
class LightEnum(metaclass=FastEnum):
ONE: 'LightEnum'
TWO: 'LightEnum'
THREE: 'LightEnum'
When this form is used it is strictly required that members are "type-hinted"
as instances of the enum class. Otherwise they will remain just as property/attributes
annotations deep inside the cls.__annotations__
- an enum could be accessed by value
LightEnum(1)
# <LightEnum.ONE: 1>
- or by name
LightEnum['ONE']
# <LightEnum.ONE: 1>
- it is possible to mix lightweight declaration and a value-provided one in the same class:
class MixedEnum(metaclass=FastEnum):
_ZERO_VALUED = 1
AUTO_ZERO: 'MixedEnum'
ONE: 'MixedEnum' = 1
AUTO_ONE: 'MixedEnum'
TWO: 'MixedEnum' = 2
MixedEnum(1)
# <MixedEnum.ONE: 1>
MixedEnum.AUTO_ZERO
# <MixedEnum.AUTO_ZERO: 0>
MixedEnum.AUTO_ONE
# <MixedEnum.ONE: 1>
When this form is used, if there are more than one Enum with the same value as a result (MixedEnum.AUTO_ONE.value
and MixedEnum.ONE.value
in this example) all subsequent enums are rendered as just aliases to the first declared
(the order of declaration is: first value-provided enums then lightweight forms so auto-valued will always become
aliases, not vice versa). The auto-valued enums value provider is independent from value-provided ones.
-
as shown in the previous example, a special attribute
_ZERO_VALUED
could be provided in class declaration; given it's value renders toTrue
in boolean context auto-valued enums will start from zero instead of integer 1; The_ZERO_VALUED
attribute is erased from the resulting enum type -
an enum creation can be hooked with 'late-init'. If a special method
def __init_late(self): ...
is provided within enum class' declaration, it's run for every enum instance created after all of them are created successfully
class HookedEnum(metaclass=FastEnum):
halved_value: 'HookedEnum'
__slots__ = ('halved_value',)
def __init_late(self):
# noinspection PyTypeChecker
self.halved_value: 'HookedEnum' = self.__class__(self.value // 2)
ZERO: 'HookedEnum' = 0
ONE: 'HookedEnum' = 1
TWO: 'HookedEnum' = 2
THREE: 'HookedEnum' = 3
HookedEnum.ZERO.halved_value
#<HookedEnum.ZERO: 0>
HookedEnum.ONE.halved_value
#<HookedEnum.ZERO: 0>
HookedEnum.TWO.halved_value
#<HookedEnum.ONE: 1>
HookedEnum.THREE.halved_value
#<HookedEnum.ONE: 1>
- enums are singletons
from pickle import dumps, loads
o = LightEnum.ONE
r = loads(dumps(o))
id(o)
# 139649196736456
id(r)
# 139649196736456
o is r
# True
- enums are hashable
list(LightEnum)
# [<LightEnum.ONE: 1>, <LightEnum.TWO: 2>, <LightEnum.THREE: 3>]
set(LightEnum)
# {<LightEnum.ONE: 1>, <LightEnum.TWO: 2>, <LightEnum.THREE: 3>}
- enums are easily extended if one needs
class ExtendedEnum(metaclass=FastEnum):
description: Text
__slots__ = ('description',)
def __init__(self, value, description, name):
self.value = value
self.name = name
self.description = description
def describe(self):
return self.description
RED = 'red', 'a color of blood'
GREEN = 'green', 'a color of grass in the spring'
ExtendedEnum.GREEN
# <ExtendedEnum.GREEN: green>
str(ExtendedEnum.GREEN)
# 'ExtendedEnum.GREEN'
ExtendedEnum.GREEN.name
# 'GREEN'
ExtendedEnum.GREEN.value
# 'green'
ExtendedEnum.GREEN.description
# 'a color of grass in the spring'
ExtendedEnum.GREEN.describe()
# 'a color of grass in the spring'
In case an enum has extended set of fields it it must be guaranteed that the __init__
method has the name
argument in the last position. It's because enum instances are
instantiated like enumtype(*value, name=name)
where value
is the right side of
assignment in the code RED = 'red', 'a color of blood'
(in case the right side is not
a tuple-like object it is wrapped into tuple beforehand)
- protected from modifications
del LightEnum.ONE
#Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "fastenum.py", line 81, in __delattr__
# self.__restrict_modification()
# File "fastenum.py", line 69, in __restrict_modification
# raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set')
#TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set
del LightEnum.ONE.name
#Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "fastenum.py", line 69, in __restrict_modification
# raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set')
#TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set
ExtendedEnum.GREEN.description = "I've changed my mind, it's a colour of swamps"
#Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "fastenum.py", line 69, in __restrict_modification
# raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set')
#TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set
- protected from subclassing
class LightSub(LightEnum):
FOUR: 'LightSub'
#Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "fastenum.py", line 34, in __new__
# typ.__call__ = typ.__new__ = typ.get
# File "fastenum.py", line 76, in __setattr__
# self.__restrict_modification()
# File "fastenum.py", line 69, in __restrict_modification
# raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set')
#TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set
- but you could declare a class providing no new values (the result will be just an alias):
class LightAlias(LightEnum):
pass
LightAlias.ONE
# <LightEnum.ONE: 1>
- and extensible in superclasses
class ExtEnumBase(metaclass=FastEnum):
description: Text
__slots__ = ('description',)
def __init__(self, value, description, name):
self.value = value
self.name = name
self.description = description
class ExtEnumOne(ExtEnumBase):
ONE = 1, 'First positive integer'
TWO = 2, 'Second positive integer'
class ExtEnumTwo(ExtEnumBase):
RED = 'red', 'A sunset'
GREEN = 'green', 'Allows to cross the road'
- as requested after publication, it's possible to subclass arbitrary classes (look at tests for more) starting from 1.3:
class IntEnum(int, metaclass=FastEnum):
ONE = 1
TWO = 2
# >>> IntEnum.ONE == 1
# True
# >>> IntEnum.ONE * 100
# 100
import sys
sys.exit(IntEnum.TWO) # sets python's interpreter exit code to 2
- faster than standard library's one
In [2]: %timeit ValuesGivenEnum.FOUR
21.4 ns ± 0.196 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [3]: %timeit ValuesGivenEnum.FOUR.name
30.3 ns ± 0.121 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [4]: %timeit ValuesGivenEnum.FOUR.value
30.4 ns ± 0.166 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [5]: %timeit ValuesGivenEnum(4)
111 ns ± 0.599 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [6]: %timeit ValuesGivenEnum['FOUR']
84.6 ns ± 0.188 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
compare that to
# Classic enum
from enum import Enum
class StdEnum(Enum):
ONE = 1
TWO = 2
In [7]: %timeit StdEnum.ONE
69.2 ns ± 0.195 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [8]: %timeit StdEnum.ONE.name
247 ns ± 0.501 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [9]: %timeit StdEnum.ONE.value
249 ns ± 1.43 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [10]: %timeit StdEnum(1)
380 ns ± 3.74 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [11]: %timeit StdEnum['ONE']
134 ns ± 0.246 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
That is:
- 3 times faster on enum's member access
- ~8.5 times faster on enum's property (
name
,value
) access - 3 times faster on enum's access by val (call on enum's class
MyEnum(value)
) - 1.5 times faster on enum's access by name (dict-like
MyEnum[name]
)