[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