
Enums are great. They allow you cleanly define a set of statically defined options. 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) The first part of this blog post does a nice job of summarizing the general use case: http://lukasz.langa.pl/8/single-dispatch-generic-functions/ 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. You might try this by writing a small subclass of Enum: class CallableEnum(Enum): def __call__(self, *args, **kwargs): return self.value(*args, **kwargs) class Implementation(CallableEnum): FOO = foo BAR = bar def call_implementation(implementation, *args, **kwargs): return implementation(*args, **kwargs) This looks great, with the Enum serving essentially as a typed namespace for a group of related functions, but unfortunately it this doesn't work. 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-a... 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. 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 would write this myself and throw it up on pypi, but there doesn't seem to be any way to do this short of delving into the guts of enum.EnumMeta <https://github.com/python/cpython/blob/0dc5c3169dcd4853612d11ed8c92b12fa210c...>. Given that I think that others will also find this pattern useful, rather than forking the enum module I would like to add this into the standard library instead. Note: this proposal would also allow defining enum values using "def" inside the Enum body. But I think this is actually pretty clean, aside from general enum confusion over the fact that Implementation.FOO is Enum instance, not the method. class Implementation(CallableEnum): def FOO(...): ... def BAR(...): ...

On Fri, Apr 14, 2017 at 06:06:29PM -0700, Stephan Hoyer wrote:
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? [...]
I think you missed a word. Making the enum instances... what? Callable? [...]
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.
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.
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

On 04/16/2017 01:24 AM, Steven D'Aprano wrote:
On Fri, Apr 14, 2017 at 06:06:29PM -0700, Stephan Hoyer wrote:
[...]
I agree with D'Aprano: such unusual usage does not belong in the stdlib. Fortunately, there is the Advanced Enumeration (aenum) library*: --- 8< ----------------------------------------------------------------- from aenum import Enum, enum class CallableEnum(Enum): def __new__(cls, *args, **kwds): member = object.__new__(cls) member._impl = args[0] if member._impl.__doc__ is not None: member._value_ = member._impl.__doc__ else: member._value_ = repr(member._impl) return member def __call__(self, *args, **kwds): return self._impl(*args, **kwds) --- 8< ----------------------------------------------------------------- and in use --- 8< ----------------------------------------------------------------- class TestEnum(CallableEnum): @enum def hello(text): "a pleasant greeting" print('hello,', text) @enum def goodbye(text): print('goodbye,', text) list(TestEnum) # [ # <TestEnum.hello: 'a pleasant greeting'>, # <TestEnum.goodbye: '<function goodbye at 0xb7264844>'>, # ] print(TestEnum.hello) # TestEnum.hello TestEnum.hello('how are you?') # 'hello, how are you?' TestEnum.goodbye('see you soon!') # 'goodbye, see you soon!' --- 8< ----------------------------------------------------------------- Note that it is possible to do the same thing using the stdlib Enum if you create your own decorator (it must return a class instance) and a slight rewrite of __new__ -- but I'll leave that as an exercise for the reader. -- ~Ethan~ * Disclosure: I am the primary author of the stdlib Enum; I am also the author if the enum34 backport and the aenum library. enum34: https://pypi.python.org/pypi/enum34 aenum: https://pypi.python.org/pypi/aenum

On Fri, Apr 14, 2017 at 06:06:29PM -0700, Stephan Hoyer wrote:
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? [...]
I think you missed a word. Making the enum instances... what? Callable? [...]
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.
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.
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

On 04/16/2017 01:24 AM, Steven D'Aprano wrote:
On Fri, Apr 14, 2017 at 06:06:29PM -0700, Stephan Hoyer wrote:
[...]
I agree with D'Aprano: such unusual usage does not belong in the stdlib. Fortunately, there is the Advanced Enumeration (aenum) library*: --- 8< ----------------------------------------------------------------- from aenum import Enum, enum class CallableEnum(Enum): def __new__(cls, *args, **kwds): member = object.__new__(cls) member._impl = args[0] if member._impl.__doc__ is not None: member._value_ = member._impl.__doc__ else: member._value_ = repr(member._impl) return member def __call__(self, *args, **kwds): return self._impl(*args, **kwds) --- 8< ----------------------------------------------------------------- and in use --- 8< ----------------------------------------------------------------- class TestEnum(CallableEnum): @enum def hello(text): "a pleasant greeting" print('hello,', text) @enum def goodbye(text): print('goodbye,', text) list(TestEnum) # [ # <TestEnum.hello: 'a pleasant greeting'>, # <TestEnum.goodbye: '<function goodbye at 0xb7264844>'>, # ] print(TestEnum.hello) # TestEnum.hello TestEnum.hello('how are you?') # 'hello, how are you?' TestEnum.goodbye('see you soon!') # 'goodbye, see you soon!' --- 8< ----------------------------------------------------------------- Note that it is possible to do the same thing using the stdlib Enum if you create your own decorator (it must return a class instance) and a slight rewrite of __new__ -- but I'll leave that as an exercise for the reader. -- ~Ethan~ * Disclosure: I am the primary author of the stdlib Enum; I am also the author if the enum34 backport and the aenum library. enum34: https://pypi.python.org/pypi/enum34 aenum: https://pypi.python.org/pypi/aenum
participants (3)
-
Ethan Furman
-
Stephan Hoyer
-
Steven D'Aprano