[Python-ideas] Callable Enum values
Steven D'Aprano
steve at pearwood.info
Sun Apr 16 04:24:33 EDT 2017
On Fri, Apr 14, 2017 at 06:06:29PM -0700, Stephan Hoyer wrote:
> One way that I've found myself using enums recently is for dispatching (as
> keys in a dictionary) between different interchangeable functions or
> classes. My code looks something like this:
>
> from enum import Enum
>
> def foo(...):
> ...
>
> def bar(...):
> ...
>
> class Implementation(Enum):
> FOO = 1
> BAR = 2
>
> _IMPLEMENTATIONS = {
> Implementation.FOO: foo,
> Implementation.BAR: bar,
> }
>
> def call_implementation(implementation, *args, **kwargs):
> return _IMPLEMENTATIONS[implementation](*args, **kwargs)
To me, this looks like a case where you want to give each instance a
custom per-instance method. (Surely this must be a named Design
Pattern?)
There's not really good syntax for that, although if the method can be
written with lambda you can at least avoid a global function:
from types import MethodType
class Colour(Enum):
RED = 1
BLUE = 2
Colour.RED.paint = MethodType(
lambda self: print("Painting the town", self.name),
Colour.RED)
Colour.BLUE.paint = MethodType(
lambda self: print("I'm feeling", self.name),
Colour.BLUE)
but of course one can write a helper function or decorator to make it a
little less ugly. See below.
This doesn't work for dunder methods, not directly. You can't say:
Colour.RED.__call__ = ...
to make the RED instance callable. But you can do this:
class Colour(Enum):
RED = 1
BLUE = 2
def __call__(self):
return self._call()
Colour.RED._call = MethodType( ... )
and now Colours.RED() will call your per-instance method.
We can use a decorator as a helper:
# untested
def add_per_instance_method(instance):
def decorator(function):
instance._call = MethodType(function, instance)
return function
return decorator
and now this should work:
@add_per_instance_method(Colour.RED)
def _call(self):
# implementation goes here
...
@add_per_instance_method(Colour.BLUE)
def _call(self):
...
The only thing left to do is clean up at the end and remove the left
over namespace pollution:
del _call
if you can be bothered. And now you have callable Enums with a
per-instance method each, as well as a pretty enumeration value for
debugging. Much nicer than <function foo at 0xb7b02cd4>.
Does this solve your problem? If not, what's missing?
[...]
> Obviously, enums are better than strings, because they're static declared
> and already grouped together. But it would be nice if we could do one
> better, by eliminating the dictionary, moving the dictionary values to the
> enum and making the enum instances.
I think you missed a word. Making the enum instances... what? Callable?
[...]
> The problem is that when you assign a function to an Enum, it treats it as
> a method instead of an enum value:
> http://stackoverflow.com/questions/40338652/how-to-define-enum-values-that-are-functions
That's a minor, and not very important, limitation. I consider that
equivalent to the restriction that functions defined inside a class body
are automatically converted to instance methods. If you want to avoid
that, you need to decorate them as a class method or static method or
similar.
> Instead, you need to wrap function values in a callable class, e.g.,
>
> from functools import partial
>
> class Implementation(CallableEnum):
> FOO = partial(foo)
> BAR = partial(bar)
>
> This is OK, but definitely uglier and more error prone than necessary. It's
> easy to forget to add a partial, which results in an accidental method
> declaration.
Python doesn't have to protect the programmer from every possible source
of human error. I don't think it is Python's responsibility to protect
people from accidentally doing something like this:
class Colours(Enum):
RED = partial(red)
BLUE = partial(blue)
GREEN = partial(green)
YELLOW = yellow # oops
Sometimes the answer is Then Don't Do That.
> It would be nice to have a CallableEnum class that works like Enum, but
> adds the __call_ method and doesn't allow defining any new methods: all
> functions assigned in the class body become Enum instances instead of
> methods.
I wouldn't be happy with the restriction "all methods are Enum
instances". Seems over-eager. It might be suitable for *your* specific
use-case, but I expect that other users of Enum will want to have both
methods and functions as Enum values:
class Thing(Enum):
WIDGET = 'x'
DOODAD = 5
GIZMO = function # how to protect this?
THINGAMAJIG = 'something'
def method(self, arg):
...
Given that wanting to use a function as the enumeration value is quite
unusual in the first place, I don't think this belongs in the standard
library.
--
Steve
More information about the Python-ideas
mailing list