A simpler Enum for Python 3.
Python
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src/simplenum
.gitignore hide diea Jun 1, 2013
MANIFEST.in
README.md
setup.py improve error detection Jun 3, 2013

README.md

simple-enum

A simpler Enum for Python 3.

Getting Started

Install simple-enum from PyPI:

easy_install simple-enum

The Dictionary Point Of View

An Enum is an ordered, immutable dictionary. You only need to provide the names:

>>> from simplenum import Enum
>>> class Colour(Enum):
...     red
...     green
...     blue
...
>>> list(Colour)
['red', 'green', 'blue']
>>> 'red' in Colour
True

By default, the dictionary values are the names themselves:

>>> Colour['red']
'red'

but you can change that using values:

>>> from simplenum import from_one
>>> class Weekday(Enum, values=from_one):
...     monday, tuesday, wednesday, thursday, friday, saturday, sunday
...
>>> Weekday['monday']
1

The Named Tuple Point Of View

An Enum is also an ordered, immutable set of named tuples:

>>> class Colour(Enum):
...     red
...     green
...     blue
...
>>> Colour.red
Colour(name='red', value='red')
>>> Colour.red.name
'red'
>>> Colour.red[1]
'red'
>>> list(Colour.items())
[Colour(name='red', value='red'), Colour(name='green', value='green'), Colour(name='blue', value='blue')]
>>> isinstance(Colour.red, Colour)
True

As before, you can specify the values:

>>> class Weekday(Enum, values=from_one):
...     monday, tuesday, wednesday, thursday, friday, saturday, sunday
...
>>> Weekday.monday.value
1
>>> Weekday.tuesday
Weekday(name='tuesday', value=2)

That's It, Really

The two points of view are consistent - you can mix and match as you please.

OK, so there is a little more below. But in most cases the above should be all you need.

Advanced Use

Retrieving Tuples

If you have a name, or value, you can get the tuple by calling the class:

>>> Weekday('wednesday')
Weekday(name='wednesday', value=3)
>>> Weekday(value=4).name
thursday

Providing Implicit Values

The values parameter expects a no-argument function (called once per class definition), which returns a second function from names to values.

So, for example, to give random values:

>>> from random import random
>>> def random_values():
...     def value(name):
...         return random()
...     return value
...
>>> class Random(Enum, values=random_values):
...     a, b, c
...
>>> list(Random.items())
[Random(name='a', value=0.49267653329514594), Random(name='b', value=0.5521902021074088), Random(name='c', value=0.5540234367417308)]

Providing Explicit Values

If you want to specify the values explicitly, use implicit=False:

>>> class Favourite(Enum, implicit=False):
...     food = 'bacon'
...     number = 7
...
>>> Favourite.food.value
bacon
>>> Favourite['number']
7

You can even go wild and mix things up (here we're using bit-fields via bits):

>>> class Emphasis(Enum, values=bits, implicit=False):
...     with implicit:
...         underline, italic, bold
...     bold_italic = italic | bold
...
>>> Emphasis.bold_italic.value
6

Aliases

By default, it is an error to repeat a value:

>>> class Error(Enum, implicit=False, values=from_one):
...     with implicit:
...         one
...     another_one = 1
...
ValueError: Duplicate value (1) for one and another_one

but you can disable the safety check with allow_aliases=True:

>>> class MulitlingualWeekday(Enum, implicit=False, values=from_one, allow_aliases=True):
...     with implicit:
...         monday, tuesday, wednesday, thursday, friday, saturday, sunday
...     lunes, martes, miercoles, jueves, viernes, sabado, domingo = \
...         monday, tuesday, wednesday, thursday, friday, saturday, sunday
...

This creates aliases - they are valid names, but they retrieve the original tuple:

>>> MulitlingualWeekday.lunes
MulitlingualWeekday(name='monday', value=1)
>>> MulitlingualWeekday('martes')
MulitlingualWeekday(name='tuesday', value=2)
>>> MulitlingualWeekday['miercoles']
3

Discussion

Background

Some time ago I wrote an intemperate rant about the standard Enum for Python 3.

Afterwards, I felt guilty. So, to atone myself, I started to modify the code, adding features that I felt missing from the original. The result was bnum.

But, as I worked on bnum, I came to see that I was not producing the consistent, elegant design that I was advocating. Instead, I was adding features to an already over-complex project.

So, after three weeks of work, I stopped. The next day I wrote this.

Differences With Enum

This project differs from PEP-0345's Enum in two important ways.

  1. The typical end-user will notice that the API defaults to implicit values. This is because I feel the most common case for an Enum requires nothing more than a set of names. That case should be as simple as possible.

  2. The Enum expert will see that I have made no effort to support other types of enumeration (alternatives to named tuples) through inheritance. In my career as a software engineer I have made many mistakes. All too often those mistakes involved inheritance. This design reflects that experience.

In addition, this code supports alternative implicit values (eg. bit fields), has no support for the "functional" form, and, by default, flags an error on duplicates.

I realise that one day's work (even when born from three weeks of frustration) is unlikely to have captured all the subtleties of the problem; that some of the complexity of the standard Enum is justified and will, in time and with bug fixes, clutter this project. But I hope that I have found something of value in the balance of features here, and that others will appreciate the view from this particular local maximum of the design space.

The Danger Of Magic

One objection to the implicit value approach used here is that it can lead to confusing errors when global names are shadowed by implicit values. However, this can be ameliorated in most cases by careful implementation.

In the case of implicit classes, like:

>>> class Error1(Enum):
...     a = sin(b)
...
ExplicitError: Implicit scope support simple names only - no assignment or evaluation of expressions

the values returned are not the implicit values used, but Explode instances which generate the given error on any access. A similar error is triggered by assignment.

The approach above, using a modified value, cannot be used for with contexts, where the value might be used later. But with contexts provide a separate mechanism for detecting and modifying errors. So here, for example, a TypeError is detected and replaced:

>>> class Error2(Enum, implicit=False):
...     with implicit:
...         a = sin(b)
...
ExplicitError: Implicit scope support simple names only - no assignment or evaluation of expressions

Technical Details

The consistency of the two viewpoints (dict and named tuples) hinges on the method dict.items(), which returns (name, value) tuples. These are both named tuples and the dictionary contents.

Implicit values are generated by providing a default value for missing class dictionary contents. This shadows global names so cannot be used to evaluate expressions - but a simple list of names does not require any evaluation.

Most of my Python programming (which I admit may be influenced by functional languages) uses common, standard data structures. An enumeration - an immutable set of names, with an optional associated set of values - does not require anything beyond that. So "what is a good design?" reduces to "how best can I fit enumerations into existing structures?" A little consideration gives two ways to associate names and values: in a dictionary, or as pairs. A little more consideration shows the two can be combined succinctly. Hence the design.

Things You Can Do With The Simpler Enum (That You Can't Do With The Standard Enum)

Have a simple list of names in "class" form:

>>> class Colour(Enum):
...     red
...     green
...     blue

Detect a stupid mistake:

>>> class Error(Enum, values=from_one):
...     with implicit:
...         one
...         two
...     three = 2
...
ValueError: Duplicate value (2) for two and three

Define bit fields:

>>> class IntEmphasis(Enum, values=bits):
...     underline
...     italic
...     bold
...
>>> allowed_styles = IntEmphasis.italic.value | IntEmphasis.bold.value

Credits

Thanks to Ethan Furman, who graciously shared his code and so educated me on the subtleties of the Python meta-class protocol.

Duncan Booth provided the implicit values hack and the motivation to question authority.

Legalities

(c) 2013 Andrew Cooke, andrew@acooke.org; released into the public domain for any use, but with absolutely no warranty.