Declarative usage of Python's enum.Enum
requires values to be provided, when in the most basic use case for an enum we don't actually care about names and values. We only care about the sentinels themselves. After reading a related Q&A recently, I realised it is possible to use the __prepare__
method of the enum's metaclass to get this kind of declaration:
class Color(Enum):
red
blue
green
And the implementation to make things so dry is actually fairly easy:
from collections import defaultdict
class EnumMeta(type):
@classmethod
def __prepare__(meta, name, bases):
return defaultdict(object)
def __new__(cls, name, bases, classdict):
classdict.default_factory = None
return type.__new__(cls, name, bases, classdict)
class Enum(metaclass=EnumMeta):
pass
In Python 3.6, there was provided enum.auto
to help with that issue of omitting values, but the interface is still strange - you're required to specify the auto()
value for each member, and inherit from a different base which fixes up the __repr__
:
class Color(NoValue):
red = auto()
blue = auto()
green = auto()
Knowing that many man-hours and great care has gone into the implementation chosen for the standard library, there must be some reason why the arguably more Pythonic version of a declarative enum demonstrated earlier doesn't work properly.
My question is, what are the problems and failure modes of the proposed approach, and why was this (or something similar) decided against - with the auto
feature being included in Python 3.6 instead?
There are several pitfalls to having a defaultdict
be the Enum's namespace:
_EnumDict
namespace:
_generate
methodAnd the most important:
Why won't it work? Not only can __prepare__
set attributes on the namespace dict, so can the namespace dict itself -- and _EnumDict
does: _member_names
, a list of all the attributes that should be members.
However, the goal of declaring a name without a value is not impossible -- the aenum
1 package allows it with a few safeguards:
property
, classmethod
, and staticmethod
are excluded by default, but one can include them and/or exclude other global namesThis behavior was deemed too magical for the stdlib, though, so if you want it, along with some other enhancements/improvements2, you'll have to use aenum
.
An example:
from aenum import AutoEnum
class Color(AutoEnum):
red
green
blue
The __repr__
still shows the created values, though.
--
1 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
2NamedConstant
(just like it says ;), NamedTuple
(metaclass based, default values, etc.), plus some built-in Enums:
MultiValueEnum
--> several values can map to one name (not aliases)NoAliasEnum
--> names with the same value are not aliases (think playing cards)OrderedEnum
--> members are order-comparable by definitionUniqueEnum
--> no aliases allowedYou may be interested that you can create enums using multiple arguments:
from enum import Enum
class NoValue(Enum):
def __repr__(self):
return '<%s.%s>' % (self.__class__.__name__, self.name)
Color = NoValue('Color', ['red', 'green', 'blue']) # no need for "auto()" calls
That way you don't have to use auto
or anything else (like __prepare__
).
why was this (or something similar) decided against - with the auto feature being included in Python 3.6 instead?
This has been discussed at length on the Python issue tracker (especially bpo-23591) and I'll include the (summarized) arguments against it:
Vedran Čačić:
This is something fundamental: it is breaking the promise that class body is a suite of commands, where Python statements (such as assignment) have their usual semantics.
Raymond Hettinger:
As long as [auto] has been defined somewhere (i.e. from enum import [auto]), it is normal Python and doesn't fight with the rest of language or its toolchains.
In short: class definitions interpret these "variables" as lookups:
class A(object):
a
but for enum
they should be interpreted as assignments? That use-case simply wasn't considered "special enough to break the rules".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With