24

I noticed an oddity in the Python 3 Enums (link).
If you set the value of an Enum to a function, it prevents the attribute from being wrapped as an Enum object, which prevents you from being able to use the cool features like EnumCls['AttrName'] to dynamically load the attribute.

Is this a bug? Done on purpose?
I searched for a while but found no mention of restricted values that you can use in an Enum.

Here is sample code that displays the issue:

class Color(Enum):
    Red = lambda: print('In Red')
    Blue = lambda: print('In Blue')

print(Color.Red)    # <function> - should be Color.Red via Docs
print(Color.Blue)   # <function> - should be Color.Bluevia Docs
print(Color['Red']) # throws KeyError - should be Color.Red via Docs

Also, this is my first time asking, so let me know if there's anything I should be doing differently! And thanks for the help!

Eliasz Kubala
  • 3,836
  • 1
  • 23
  • 28
C. Loew
  • 293
  • 1
  • 2
  • 7

6 Answers6

13

You can override the __call__ method:

from enum import Enum, auto

class Color(Enum):
    red = auto()
    blue = auto()

    def __call__(self, *args, **kwargs):
        return f'<font color={self.name}>{args[0]}</font>'

Can then be used:

>>> Color.red('flowers')
<font color=red>flowers</font>
K3---rnc
  • 6,717
  • 3
  • 31
  • 46
10

The documentation says:

The rules for what is allowed are as follows: _sunder_ names (starting and ending with a single underscore) are reserved by enum and cannot be used; all other attributes defined within an enumeration will become members of this enumeration, with the exception of __dunder__ names and descriptors (methods are also descriptors).

A "method" is just a function defined inside a class body. It doesn't matter whether you define it with lambda or def. So your example is the same as:

class Color(Enum):
    def Red():
        print('In Red')
    def Blue():
        print('In Blue')

In other words, your purported enum values are actually methods, and so won't become members of the Enum.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • It's annoying as this feels like a nice way to switch on functions, I guess perhaps you could define the enum and @singledispatch on the values, all these alternatives seem verbose... – Andy Hayden Nov 08 '17 at 00:14
  • 3
    @BrenBarn I think OP prematurely accepted this answer. The linked doc on enum states `enumerations can have arbitrary values` as long as they follow naming restrictions. Since OP did not violate the naming restriction, why is OP unable to assign a function as the value of an enum? – Razzle Shazl Dec 03 '20 at 01:50
  • @BrenBarn From testing, `Red` and `Blue` are actually static class functions, not class methods as your answer states. – Razzle Shazl Dec 03 '20 at 02:06
  • @BrenBarn please see my answer, would love your feedback https://stackoverflow.com/a/65119345/2359945 – Razzle Shazl Dec 03 '20 at 03:14
  • 1
    @RazzleShazl: All methods will appear as "class functions" if you access them on the class. A "method" is just a function object assigned as a class attribute. That's more or less the point of my answer: The documentation I linked to explains that descriptors (including functions) are an exception to the "arbitrary values" statement. – BrenBarn Dec 03 '20 at 03:41
  • Ah I missed the part `descriptors (methods are also descriptors)`, got it. As for the method vs function comment, it was confusing for me because your answer differed from my python print testing. I figured you would update your answer. Thanks for explaining this! – Razzle Shazl Dec 03 '20 at 04:10
5

If someone need/want to use Enum with functions as values, its possible to do so by using a callable object as a proxy, something like this:

class FunctionProxy:
    """Allow to mask a function as an Object."""
    def __init__(self, function):
        self.function = function

    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)

A simple test:

from enum import Enum
class Functions(Enum):
    Print_Function = FunctionProxy(lambda *a: print(*a))
    Split_Function = FunctionProxy(lambda s, d='.': s.split(d))

Functions.Print_Function.value('Hello World!')
# Hello World!
Functions.Split_Function.value('Hello.World.!')
# ['Hello', 'World', '!']
Ceppo93
  • 1,026
  • 10
  • 11
  • 2
    Another strategy is to use `functools.partial` which avoids having to define a new class. – rodrigob Sep 16 '19 at 16:23
  • I would recomment adding `def __repr__(self): return repr(self.function)` for debugging purposes, and updating metadata. Like this: https://stackoverflow.com/a/58714331/7262247 – smarie May 28 '20 at 20:52
5

You can also use functools.partial to trick the enum into not considering your function a method of Color:

from functools import partial
from enum import Enum

class Color(Enum):
    Red = partial(lambda: print('In Red'))
    Blue = partial(lambda: print('In Blue'))

With this you can access name and value as expected.

Color.Red
Out[17]: <Color.Red: functools.partial(<function Color.<lambda> at 0x7f84ad6303a0>)>
Color.Red.name
Out[18]: 'Red'
Color.Red.value()
In Red
Darkdragon84
  • 539
  • 5
  • 13
0

I ran into this issue recently, found this post, and first was tempted to use the wrapper pattern suggested in the other related post. However eventually I found out that this was a bit overkill for what I had to do. In the past years this happened to me several times with Enum, so I would like to share this simple experience feedback:

if you need an enumeration, ask yourself whether you actually need an enum or just a namespace.

The difference is simple: Enum members are instances of their host enum class, while namespace members are completely independent from the class, they are just located inside.

Here is an example of namespace containing callables, with a get method to return any of them by name.

class Foo(object):
    """ A simple namespace class with a `get` method to access members """
    @classmethod
    def get(cls, member_name: str):
        """Get a member by name"""
        if not member_name.startswith('__') and member_name != 'get':
            try:
                return getattr(cls, member_name)
            except AttributeError:
                pass
        raise ValueError("Unknown %r member: %r" % (cls.__name__, member_name))

    # -- the "members" --

    a = 1

    @staticmethod
    def welcome(name):
        return "greetings, %s!" % name

    @staticmethod
    def wave(name):
        return "(silently waving, %s)" % name


w = Foo.get('welcome')
a = Foo.get('a')
Foo.get('unknown')  # ValueError: Unknown 'Foo' member: 'unknown'

See also this post on namespaces.

smarie
  • 4,568
  • 24
  • 39
  • I see you've been through enum hell and came out with a robust solution. :) With your experience, could you see what I might be missing with my simple answer? (https://stackoverflow.com/a/65119345/2359945) I'm worried that I'm missing some edge case etc. Thanks! – Razzle Shazl Dec 03 '20 at 03:17
  • 1
    Hi @RazzleShazl , it seems that BrenBarn pointed at least one issue with your answer: your enum members are tuples, not functions. – smarie Dec 03 '20 at 08:25
-1

Initially, I thought your issue was just missing commas because I got the output you were expecting.:

from enum import Enum

class Color(Enum):
    Red = lambda: print('In Red'),
    Blue = lambda: print('In Blue'),

print(Color.Red)
print(Color.Blue)
print(Color['Red'])

output (python3.7)

$ /usr/local/opt/python/bin/python3.7 ~/test_enum.py
Color.Red
Color.Blue
Color.Red

@BernBarn was kind enough to explain that in my solution that a tuple is being created, and to invoke the function would require dereferencing value[0]. There is already another answer using value[0] in this way. I miss rb for this.

Razzle Shazl
  • 1,287
  • 1
  • 8
  • 20
  • 1
    By including a comma, you are creating a tuple. This means the value of the enum is no longer actually a function, but a tuple containing a function. You can see that the value is not actually a function because, e.g., `Color.Red.value()` will not work. You would need to do `Color.Red.value[0]()`. However, depending on the OP's needs, wrapping the function in a tuple could be a workable way to get it into the enum; it just means you need to do an extra step to unwrap it before you can call it. – BrenBarn Dec 03 '20 at 03:44
  • Thanks @BrenBarn for quickly providing feedback and for clearing up this for me! – Razzle Shazl Dec 03 '20 at 04:38