True immutable symbolic enumeration with qualified value access.
| Python |
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 | def Enum(*names):
##assert names, "Empty enums are not supported" # <- Don't like empty enums? Uncomment!
class EnumClass(object):
__slots__ = names
def __iter__(self): return iter(constants)
def __len__(self): return len(constants)
def __getitem__(self, i): return constants[i]
def __repr__(self): return 'Enum' + str(names)
def __str__(self): return 'enum ' + str(constants)
class EnumValue(object):
__slots__ = ('__value')
def __init__(self, value): self.__value = value
Value = property(lambda self: self.__value)
EnumType = property(lambda self: EnumType)
def __hash__(self): return hash(self.__value)
def __cmp__(self, other):
# C fans might want to remove the following assertion
# to make all enums comparable by ordinal value {;))
assert self.EnumType is other.EnumType, "Only values from the same enum are comparable"
return cmp(self.__value, other.__value)
def __invert__(self): return constants[maximum - self.__value]
def __nonzero__(self): return bool(self.__value)
def __repr__(self): return str(names[self.__value])
maximum = len(names) - 1
constants = [None] * len(names)
for i, each in enumerate(names):
val = EnumValue(i)
setattr(EnumClass, each, val)
constants[i] = val
constants = tuple(constants)
EnumType = EnumClass()
return EnumType
if __name__ == '__main__':
print '\n*** Enum Demo ***'
print '--- Days of week ---'
Days = Enum('Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su')
print Days
print Days.Mo
print Days.Fr
print Days.Mo < Days.Fr
print list(Days)
for each in Days:
print 'Day:', each
print '--- Yes/No ---'
Confirmation = Enum('No', 'Yes')
answer = Confirmation.No
print 'Your answer is not', ~answer
|
Discussion
Most propositions for an enum in python attempt to solve the issue with a single class. However, fact is that enum has a dual nature: It declares a new anonimous type and all possible instances (values) of that type at the same time. In other words, there is a distinction between an enum type and its associated values.
In recognition of this fact, this recipe uses two python classes and python's nested scopes to accomplish a clean and concise implementation.
Note that - Enums are immutable; attributes cannot be added, deleted or changed. - Enums are iterable. - Enum value access is symbolic and qualified, ex. Days.Monday (like in C#). - Enum values are true constants. - Enum values are comparable. - Enum values are invertible (usefull for 2-valued enums, like Enum('no', 'yes'). - Enum values are usable as truth values (in a C tradition, but this is debatable). - Enum values are reasonably introspecitve (by publishing their enum type and numeric value)
Cheers and happy enumerating!
[See also]
Recipe "Enums for Python" by Will Ware http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/67107
Recipe "Enumerated values by name or number" by Samuel Reynolds http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305271


Comments
Small nit. Nice solution, the best so far. My only issue is the interaction between enumerations when you mistakenly compare them:
Changing the __cmp__ method on EnumValue to do a == instead of cmp changes the results in a more comfortable way (No == No and No != Yes) but is still uncomfortable
I believe two constants from two different enums should not be comparable at all, there is no sense in mixing apples and oranges. A solution is replacing the cmp with the following:
In that case you end up comparing only similar things.
You're perfectly right ... ... different enums should not be comparable at all.
The question is: Is it better to assert that, or always treat values from different enums as different, as you propose.
A flaw of the latter is that cmp cannot really tell if an EnumType is less or greater then another, so that for ex. sorting a list of different enums would result in random ordering, which might not be a lucky result.
So, probably it's better to assert then to guess, I guess. Adding the assertion to the recipe ...
... though this breaks C tradition. ... as C does allow you to compare apples and oranges, but I'm the last to complain about it.
Here's a small potential improvement: If you add:
right after the commented-out assertion, then you can just use a space-separated string for the enum values:
The user could of course do this manually, but it would be nice for the Enum to automatically do it.
I generally don't favor "omnipotent" code - meaning code that claims to deal with "everything". This kind of approach always turned against me. Instead, I favor clear separation of concerns and responsibilities, according to the maxim "do one thing, and do it well".
Anyway, I doubt whether a "simplification" of (the already very simple)
is worth the price of introducing a special case. What if someone liked
I think this is best left up to the user.
You're probably right. Nothing's stopping the user from making a wrapper function to do that kind of thing.
Enums from lists. I'm using this recipe, and would like to have enumerations built of existing lists of strings.
However, when I have anything other than explicit function arguments in the call to Enum, python 2.4.1 chokes on
This seems a little counter to what http://docs.python.org/ref/slots.html says, so, if you have any insights, I'd like to hear it.
Also,
constants = tuple(constants)
three lines from the end, is a mysterious line. How does tuple-ifying the contants list affect the return value?
Great code!
Thanks, Christopher. I'm glad you liked this recipe.
About __slots__ = names choking: Unfortunatly, from your description I couln't tell what went wrong, but maybe this would help on using string lists with Enum:
Note that assignments to __slots__ are checked by the python interpreter - a fact that is not documented at http://docs.python.org/ref/slots.html. Perhaps this explains the problem.
And now for the mysterious line
I know it does not seem to add any functionality, so it might seem a bit strange at first sight. At a second sight, it does add a value by revealing a certain intention: By making 'constants' imutable, it expresses that the content of 'constants' will not change from that point on, i.e. it really is what it says - a constant. (However, it is said that intention revealing code, while easy to read, is very hard to write, and that line of code is yet another proof. Have a look at what Martin Fowler says about the subject at http://www.martinfowler.com/articles/designDead.html )
Note the dull thud. Days = Enum(*dayNames) # note the asterisk in front of 'dayNames'
^^^^^^^^^^^^^
...as the cluestick impacts the source of ignorance. :)
Can't be picked. I can't pickle an instance of the Enum type; pickle complains that __getstate__ must be defined if __slots__ is defined.
license for this code ?
I have the same problem. I understand the fix is to add a proper __getstate__ __setstate__ function to the class, this is however a bit beyond my current python knowledge.
I spent a few hours looking at the pickling problem and it is indeed non trivial. You can start by adding __getstate__ __setstate__ functions, but that won't help because the classes are nested inside the Enum function.
Being that they use __slots__ (a class variable) my understanding is that you can't make them non nested without them stepping on each other. i.e. every time you call the Enum() function you are creating two new EnumClass and EnumValue classes (that are distinguished from the others by their scoping)
Nice recipe but I think the whole exercise is a misguided. Enums are a great tool for staticly compiled languages and for languages without namespaces.
Why provide more ways to do it when Python already has several tools that work fine? We routinely use module namespaces such as re.IGNORECASE. Likewise, it's already trivially easy to build constants with a simple class:
class Status: open, pending, closed = range(3)
As soon as you hide this simple declaration behind a wrapper like Enum(), you lose transparency. It stops being immediately obvious that an Enum() can be pickled, or compared, or used as a dictionary key, or will pass isinstance(e, numbers.Number), or pass operator.isNumberType(e). If you subclass an enumeration, the __slots__ don't automatically carry forward and you end-up with instance dictionaries and whatnot. Too many unexpected behaviors.
Coming from Java or C where Enums are a way of life, it is hard to leave those habits behind and program Pythonically. But, not everything in the world of static compilation makes sense in the python world. These Enum() objects are slower than constants assigned to a variable name or module namespace. The are also awkward in that those objects are imbued with behaviors than are a little different than straight int or string constants. As such, they place an additional burden on the reader, a burden that doesn't exist with a simpler, more basic Pythonic style.
Sign in to comment