4

I have an Enum class in Python 3.7 defined as such:

# activation functions
def relu(x: float) -> float:
    return x * (x > 0)


def sigmoid(x: float) -> float:
    return 1 / (1 + math.exp(-x))


class Activation(Enum):
    SIGMOID = sigmoid
    RELU = relu

    def __call__(self, x):
        return self.value(x)

    @classmethod
    def values(cls):
        return [function.value for function in cls]

I have tried some approaches from other similar questions online, like list(Activation) and Activation.values() but always get an empty list. Any new ideas?

bad_coder
  • 11,289
  • 20
  • 44
  • 72
SamuelNLP
  • 4,038
  • 9
  • 59
  • 102
  • 3
    https://stackoverflow.com/questions/40338652/how-to-define-enum-values-that-are-functions – Samwise May 03 '20 at 17:28
  • Hum, so my problem is not about the values. Nice. :D – SamuelNLP May 03 '20 at 17:30
  • 1
    The funny thing about this is that the function calls in enums work perfectly. – SamuelNLP May 03 '20 at 17:31
  • The enum documentation states that values can be of any type. There is a chapter about iteration and the behaviour of this program is different. In my opinion this should be reported as a bug. – VPfB May 03 '20 at 19:04
  • 1
    @VPfB: The documentation states that descriptors are not converted to members, and methods and functions are descriptors. This is not a bug. – Ethan Furman May 04 '20 at 01:02
  • @EthanFurman Well yes, I have found it in the section "Allowed members and attributes of enumerations". Thank you for correcting me. But please check the note in "Creating an Enum" (https://docs.python.org/3/library/enum.html#creating-an-enum) which states: _"Member values can be anything: int, str, etc."_ That is what I wrote in my comment now proved incorrect. – VPfB May 04 '20 at 05:58
  • @VPfB: Good point, I'll get that fixed. – Ethan Furman May 04 '20 at 15:54
  • 1
    @EthanFurman Many thanks for all your work on the enum module. – VPfB May 04 '20 at 16:40

2 Answers2

2

If you use the aenum1 library you can use its member descriptor:

from aenum import Enum, member

class Activation(Enum):

    SIGMOID = member(sigmoid)
    RELU = member(relu)

    def __call__(self, x):
        return self.value(x)

    @classmethod
    def values(cls):
        return [function.value for function in cls]

Which could also be written as:

class Activation(Enum):

    @member
    def SIGMOID(x: float) -> float:
        return 1 / (1 + math.exp(-x))

    @member
    def RELU(x: float) -> float:
        return x * (x > 0)

    def __call__(self, x):
        return self.value(x)

    @classmethod
    def values(cls):
        return [function.value for function in cls]

1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • This last one looks so much better. Thank you. – SamuelNLP May 04 '20 at 08:02
  • Then again we don't need the classmethod right? A `list(Activation)` will do, correct? – SamuelNLP May 04 '20 at 08:04
  • @SamuelNLP list(Activation) would give you a list of Enum members (name, value) each being a singleton Enum class instance. If you only want the list of Enum values the correct way is implementing the classmethod, or populating a list comprehension. In this case, implementing the method saves verbose rewriting of the comprehension outside the Enum, which becomes more relevant if you write more elaborate versions of the method. – bad_coder May 04 '20 at 12:00
  • 2
    @bad_coder: Thanks! But it must have been fleeting as I don't see it now -- but I must be close! :-) – Ethan Furman May 04 '20 at 16:04
1

There's one way to solve this that wasn't mentioned in the linked post without using functools, staticmethod, or a wrapper function.

The problem using a function definition as the only value of the Enum member, is that the Enum's __init__ and __new__ never get called. Having additional values, the Enum does get initialized as usual.

Proof as follows:

import math
from enum import Enum


def relu(x: float) -> float:
    return x * (x > 0)


def sigmoid(x: float) -> float:
    return 1 / (1 + math.exp(-x))


class Activation(Enum):

    SIGMOID = 1, sigmoid
    RELU = 2, relu

    def __init__(self, *args):
        self.your_func = args[1]

    def __call__(self, x):
        return self.your_func(x)

    @classmethod
    def values(cls):
        return [member.your_func for member in cls]

print(Activation.SIGMOID(2))
print(Activation.RELU(2))
# 0.8807970779778823
# 2

for one_func in Activation.values():
    print(one_func(2))
# 0.8807970779778823
# 2

I don't think this is a bug (as suggested in the comments) because:

However the issue with functions is that they are considered to be method definitions instead of attributes

What the several solutions have in common is encapsulating the function definition during the Enum (class syntax) value declaration.

Because of the above, using a "wrapper function" without staticmethod or functools wouldn't work, because it would never be called. Try substituting the following (neither __init__ not __call__ would be called):

SIGMOID = sigmoid
RELU = relu

def __call__(self, *args, **kwargs):
    print("inside")
    return self.f(*args, **kwargs)

In conclusion, perhaps the most pythonic approach is wrapping the function definition in a list at declaration, and unwrapping in __init__ at initialization:

import math
from enum import Enum


def relu(x: float) -> float:
    return x * (x > 0)


def sigmoid(x: float) -> float:
    return 1 / (1 + math.exp(-x))


class Activation(Enum):

    SIGMOID = [sigmoid]
    RELU = [relu]

    def __init__(self, your_func):
        self.your_func = your_func[0]

    def __call__(self, x):
        return self.your_func(x)

    @classmethod
    def values(cls):
        return [member.your_func for member in cls]


print(Activation.SIGMOID(2))
print(Activation.RELU(2))
# 0.8807970779778823
# 2

for one_func in Activation.values():
    print(one_func(2))
# 0.8807970779778823
# 2

Which saves calling staticmethod (some consider its use unpythonic outside a class definition). And saves importing and calling functools.partial - it involves unnecessary function call overhead with every member access.

Thus, the above approach might arguably be the most pythonic solution.

bad_coder
  • 11,289
  • 20
  • 44
  • 72