/fastenum

A fast Enum implementation for Python

Primary LanguagePythonMIT LicenseMIT

fast_enum

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 and value properties; the name property is a string containing the class member's name itself; the value 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 to True 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])