Mar 2, 2020 Tags: devblog, programming, python
Yet another library announcement, and a Python one at that: ordered_enum.
ordered_enum solves a singular problem: providing a totally-ordered enumeration type in Python.
Piggybacking on Python’s lovely enum.Enum
, it provides two independent ordering solutions: one
based on definition order (OrderedEnum
), and another based on value order (ValueOrderedEnum
).
Because Python’s enum.Enum
does not provide ordering by default, ostensibly to eliminate C-style
(mis-)treatment of enumerations as thin wrappers over integral types. ordered_enum re-adds
ordering to Python’s enums without exposing value restrictions.
More concretely: enums are a nice way to encapsulate abstract state. Ordering supplies a notion of “forwardness” or “weight” in a set of enumerated states. This has a number of useful applications, like sorting complex objects by an enumerated property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from ordered_enum import OrderedEnum
class Shape(OrderedEnum):
Circle = 1
Square = 2
Triangle = 3
class Foo:
def __init__(self, shape):
self.shape = shape
foos = [
Foo(Shape.Triangle), Foo(Shape.Circle), Foo(Shape.Triangle), Foo(Shape.Square)
]
sorted(foos, key=lambda foo: foo.shape)
or propagating a state machine via some notion of “forwardness”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from ordered_enum import OrderedEnum
class State(OrderedEnum):
Created: int = 0
Waiting: int = 1
Running: int = 2
Done: int = 3
def tick():
state = State.Created
while tick():
# Our enum's ordering allows us to use this as a shorthand for
# `state == State.Created or state == State.Waiting or state == State.running`
if state < State.Done:
pass
ordered_enum provides two totally-ordered enum classes, both of which inherit from enum.Enum
:
OrderedEnum
provides total ordering by definition, meaning that the ordering of members
is dependent on their order of definition within the class.ValueOrderedEnum
provides total ordering by value, meaning that the values of enum
members are compared to provide an ordering.Observe that OrderedEnum
is the more permissive of the two: because it uses only
the definition order, the values defined in an OrderedEnum
can be of heterogeneous types. That
means that this, even if ill-advised, works perfectly well:
1
2
3
4
5
6
7
8
9
10
11
12
from ordered_enum import OrderedEnum
class Animal(OrderedEnum):
Cat = "cat"
Dog = 2
Iguana = {"dictionaries": "can be values too"}
Human = {"and", "so", "can", "sets"}
Animal.Human > Animal.Cat # True
Animal.Iguana > Animal.Human # False
By contrast, ValueOrderedEnum
assumes the uniqueness (and comparability) of its
values. The former can be strongly enforced with the @enum.unique
decorator, but is not by
default:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import enum
from ordered_enum import ValueOrderedEnum
@enum.unique
class Humor(ValueOrderedEnum):
Blood = 4
Phlegm = 3
YellowBile = 2
BlackBile = 1
Humor.Blood > Humor.Phlegm # True
Humor.BlackBile > Humor.YellowBile # False
Both OrderedEnum
and ValueOrderedEnum
essentially boil down to a single custom method: __lt__
.
Combined with
functools.total_ordering
and the fact that enum.Enum
provides __eq__
, each has a straightforward implementation.
For OrderedEnum
:
1
2
3
4
5
6
7
8
9
10
11
12
@functools.total_ordering
class OrderedEnum(Enum):
@classmethod
@functools.lru_cache(None)
def _member_list(cls):
return list(cls)
def __lt__(self, other):
if self.__class__ is other.__class__:
member_list = self.__class__._member_list()
return member_list.index(self) < member_list.index(other)
return NotImplemented
…and for ValueOrderedEnum
:
1
2
3
4
5
6
@functools.total_ordering
class ValueOrderedEnum(Enum):
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
ordered_enum is available on PyPI and installable via
pip
. It supports Python 3.6 and newer:
1
$ pip3 install ordered_enum