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.
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.
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.
print e.NOT_OK
will generate ...
Adding e.NOT_OK = 1... OK! 1
If you add this in:
It will prevent you from adding new enum keys in the way that you are describing.
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?
Hmm... Using a
namedtuple()
has mostly the same effect, but allows you to create an enum such thatenum.OK
has the same value asenum.CANCEL
. The main reason I came up with this was to learn more about class creation using thetype()
function and duck-punching.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)
An example where
enum.OK
has the same value as `enum.CANCEL:I wasn't really trying to pass off
__main__
as test code. If you want some tests, try this (py.test framework):