Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using __prepare__ for an Enum ... what's the catch?

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?

like image 773
wim Avatar asked May 08 '17 18:05

wim


2 Answers

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.

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 definition
  • UniqueEnum --> no aliases allowed
like image 173
Ethan Furman Avatar answered Nov 01 '22 05:11

Ethan Furman


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

like image 33
MSeifert Avatar answered Nov 01 '22 05:11

MSeifert