Welcome, guest | Sign In | My Account | Store | Cart

Here's a fun method of creating enumerations in Python using dynamic class creation and duck-punching (monkey-patching). It works by creating a class called enum using the type metaclass. Duck-punching is then used to add properties to the class. the fget method of the property returns the enum value, but the fset and fdel methods throw exceptions, keeping the enumeration immutable. You can have enum values assigned automatically in sequence, assign them yourself, or have a mix of both.

Python, 87 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/usr/bin/env python

def enum(*sequential, **named):
    # Check for duplicate keys
    names = list(sequential)
    names.extend(named.keys())
    if len(set(names)) != len(names):
        raise KeyError('Cannot create enumeration with duplicate keys!')

    # Build property dict
    enums = dict(zip(sequential, range(len(sequential))), **named)
    if not enums:
        raise KeyError('Cannot create empty enumeration')

    if len(set(enums.values())) < len(enums):
        raise ValueError('Cannot create enumeration with duplicate values!')

    # Function to be called as fset/fdel
    def err_func(*args, **kwargs):
        raise AttributeError('Enumeration is immutable!')

    # function to be called as fget
    def getter(cls, val):
        return lambda cls: val

    # Create a base type
    t = type('enum', (object,), {})

    # Add properties to class by duck-punching
    for attr, val in enums.iteritems():
        setattr(t, attr, property(getter(t, val), err_func, err_func))

    # Return an instance of the new class
    return t()


if __name__ == "__main__":
    """A small ammount of code to demo the functionality"""
    try:
        print 'Creating empty enum...',
        e = enum()
        print 'OK!'
    except KeyError as e:
        print 'ERROR:', e

    try:
        print 'Creating enum with duplicate keys...',
        e = enum('OK', 'OK', OK=2)
        print 'OK!'
    except KeyError as e:
        print 'ERROR:', e

    try:
        print 'Creating enum with duplicate values...',
        e = enum(OK=1, PASS=1)
        print 'OK!'
    except ValueError as e:
        print 'ERROR:', e

    try:
        print 'Creating valid enum...',
        e = enum('OK', 'CANCEL', 'QUIT', test=4, ok='YES')
        print 'OK!'
    except Exception as e:
        print 'ERROR:', e

    # Immutable?
    try:
        print 'Changing e.OK = "ASDF"...',
        e.OK = 'ASDF'
        print 'OK!'
    except AttributeError as ex:
        print 'ERROR:', ex

    try:
        print 'Deleting e.OK...',
        del e.OK
        print 'OK!'
    except AttributeError as ex:
        print 'ERROR:', ex

    print e
    print e.OK
    print e.CANCEL
    print e.QUIT
    print e.test
    print e.ok

The biggest advantage to this method of doing enumerations as opposed to other ways is that this keeps the enumeration immutable.

class foo():
   OK = 1
   CANCEL = 0

>>> print foo.OK
1
>>> foo.OK = o
>>> print foo.OK == foo.CANCEL
true

This implementation is also very short. You can also assign the enum values with this method, in other implementations you cannot.

6 comments

Dave Bailey 9 years, 3 months ago  # | flag

Josh,

I like it! I have not convinced myself if this is a bug or a feature. You can have duplicate values buy adding new enum names and values later.

try:
    print 'Adding e.NOT_OK = 1...',
    e.NOT_OK = 1
    print 'OK!'
except AttributeError as ex:
    print 'ERROR:', ex

print e.NOT_OK

will generate ...

Adding e.NOT_OK = 1... OK! 1

Josh Friend (author) 9 years, 3 months ago  # | flag

If you add this in:

# Override __setattr__ so new keys can't be added
t.__setattr__ = err_func

It will prevent you from adding new enum keys in the way that you are describing.

Charlie Clark 9 years, 3 months ago  # | flag

I think key / value checks at the top of the function are a little vague. You can check for empty sequences or mappings before trying to anything with them. But basically why do you think that this approach is preferable to using a named tuple?

Josh Friend (author) 9 years, 3 months ago  # | flag

Hmm... Using a namedtuple() has mostly the same effect, but allows you to create an enum such that enum.OK has the same value as enum.CANCEL. The main reason I came up with this was to learn more about class creation using the type() function and duck-punching.

Charlie Clark 9 years, 3 months ago  # | flag

Using namedtuple() is much more concise. And as an example of using it for enums is the Standard Library I think it should be referred to: http://docs.python.org/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields

I am not sure that your assertion that duplicate values are possible holds water. Can you provide an example?

Nothing wrong with wanting to demonstrate "duck punching", something that I've never come across before but it's sort of buried in this recipe. The body of __main__ is also a bit weird in the use of try, except with lots of print statements that will never execute because the exception is raised. These are really a set of assertions. Pity you can't write assert isinstance(enum(), KeyError)

Josh Friend (author) 9 years, 3 months ago  # | flag

An example where enum.OK has the same value as `enum.CANCEL:

from collections import namedtuple
Enum = namedtuple('Enum', ['OK', 'CANCEL'], rename=False)
enum = Enum(1, 1)
assert enum.OK != enum.CANCEL

I wasn't really trying to pass off __main__ as test code. If you want some tests, try this (py.test framework):

#!/usr/bin/env python
from enum import enum
import pytest

def test_empty_enum_throws_key_error():
    with pytest.raises(KeyError):
        e = enum()

def test_enum_with_duplicate_keys_throws_key_error():
    with pytest.raises(KeyError):
        e = enum('OK', 'OK', OK=1)

def test_enum_with_duplicate_values_throws_value_error():
    with pytest.raises(ValueError):
        e = enum(OK=1, CANCEL=1)
    # Booleans hash as integer 0/1
    with pytest.raises(ValueError):
        e = enum(OK=1, CANCEL=True)
    with pytest.raises(ValueError):
        e = enum(OK=0, CANCEL=False)

def test_change_enum_value_throws_attribute_error():
    with pytest.raises(AttributeError):
        e = enum('OK', 'CANCEL')
        e.OK = 9001
    # Should also be immune to setattr()
    with pytest.raises(AttributeError):
        e = enum('OK', 'CANCEL')
        setattr(e, 'OK', 1234)

def test_add_enum_value_throws_attribute_error():
    with pytest.raises(AttributeError):
        e = enum('OK', 'CANCEL')
        e.NEW = 9001
    # Should also be immune to setattr()
    with pytest.raises(AttributeError):
        e = enum('OK', 'CANCEL')
        setattr(e, 'NEW', 1234)

def test_delete_enum_value_throws_attribute_error():
    with pytest.raises(AttributeError):
        e = enum('OK', 'CANCEL')
        del e.OK

def test_create_valid_enums():
    try:
        # Sequential
        assert enum('OK', 'CANCEL')
        # Named
        assert enum(OK=0, CANCEL=1)
        # Mix of both
        assert enum('OK', 'CANCEL', PASS=2)
        # Various value types
        assert enum('OK', YES='YES', FLOAT=1.234, INT=100)
    except:
        assert False

def test_read_enum_values():
    try:
        e = enum('OK', 'CANCEL', PASS='PASS', PI=3.14)
        assert e.OK == 0
        assert e.CANCEL == 1
        assert e.PASS == 'PASS'
        assert e.PI == 3.14
    except:
        assert False
Created by Josh Friend on Thu, 18 Oct 2012 (MIT)
Python recipes (4591)
Josh Friend's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks