10

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?

Community
  • 1
  • 1
wim
  • 338,267
  • 99
  • 616
  • 750

2 Answers2

8

There are several pitfalls to having a defaultdict be the Enum's namespace:

  • unable to access anything but other enum members/methods
  • typos create new members
  • lose protections form _EnumDict namespace:
    • overwriting members
    • overwriting methods
    • the newer _generate method

And the most important:

  • it will not work

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 aenum1 package allows it with a few safeguards:

  • magic auto behavior is only present while defining members (as soon as a normal method is defined it turns off)
  • property, classmethod, and staticmethod are excluded by default, but one can include them and/or exclude other global names

This 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.

2 NamedConstant (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 definition
  • UniqueEnum --> no aliases allowed
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Interesting, thanks! I've just had a brief glance at `AutoEnum` implementation but it got deep pretty quickly with the `_generate_next_value_` stuff. Is it fair to say the metaclass uses the same basic trick as described in my question, or is it a completely different implementation? – wim May 08 '17 at 19:29
  • @wim: Similar: `defaultdict` uses a `__missing__` method, while `_EnumDict` catches the `__getitem__` lookup and handles it from there. – Ethan Furman May 08 '17 at 19:32
  • 1
    @wim: A bunch of the complexity in `aenum` is supporting the Python 2.x series. I opened the stdlib version of `Enum` a couple days ago and it was like a breath of fresh air! – Ethan Furman May 08 '17 at 19:36
  • Hm, not a single reference to `aenum` in the docs. It seems like a good reference to have there for users who might wish for something a bit more magical. – Dimitris Fasarakis Hilliard May 09 '17 at 14:56
  • 1
    @JimFasarakisHilliard: The Python docs are not in the habit of referring to outside projects (I believe there are only a couple exceptions). Maybe the wiki, though... – Ethan Furman May 09 '17 at 15:26
1

You 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".

MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 1
    Yeah, I'm aware of the functional interface (and I don't like it)... that's actually why the question starts like: "*Declarative* usage of Python's enum...". +1 for the links to the relevant discussion, though – wim May 08 '17 at 21:16