'Python: overloading the `__call__` dunder method of a class that inherits Enum
Consider the following:
from enum import Enum
class A(int, Enum):
def __new__(cls, value):
def call(cls, value):
print("HELLO 1" + str(value))
self = super().__new__(cls, value)
self.__call__ = call
return self
def __call__(cls, value):
print("HELLO 2" + str(value))
class B(A):
NULL = 0
BOB = 1
ALICE = 2
def __call__(cls, value):
print("HELLO 3" + str(value))
print("int: " + str(B( 1)))
print("str: " + str(B("BOB")))
running this, you get:
int: B.BOB
Traceback (most recent call last):
File "/obfuscated_path/playground.py", line 27, in <module>
print("str: " + str(B("BOB")))
File "/usr/lib/python3.10/enum.py", line 384, in __call__
return cls.__new__(cls, value)
File "/usr/lib/python3.10/enum.py", line 701, in __new__
raise ve_exc
ValueError: 'BOB' is not a valid B
I don't understand why NONE of the alternative __call__
methods are actually called, and the parent's method (ie, EnumMeta.__call__()
) is directly called instead.
How can I overload the __call__
method in a child class of Enum ?
Solution 1:[1]
Thanks to the lovely folks in the Python discord channel (in #help-pancakes), I understood what the problem was.
In a nutshell, the problem was confusing Enum as a subclass of EnumMeta, when Enum is actually an instance of EnumMeta. Here, that means that when writing B("BOB")
, even though it looks like you're calling the B.__new__
constructor, or that you're calling a classmethod
-like B.__call__()
, you're actually calling A.__call__()
(in fact, __call__
is only ever used for instances).
Taking things one step further, you can consider the following
class MetaClassName(type):
def __new__(cls, clsname, superclasses, attributedict):
return super(MetaClassName, cls).__new__(cls,
clsname,
superclasses,
attributedict,
)
def __call__(cls, value):
print("MetaClassName.__call__(): " + str(cls))
class ClassName(metaclass=MetaClassName):
def __new__(cls, value):
print("ClassName.__new__(): " + str(cls))
return int.__new__(cls, value + 1)
my_instance = ClassName(3)
print(my_instance)
which prints:
MetaClassName.__call__(): <class '__main__.ClassName'>
None
As you can see, ClassName.__new__()
is never called. In the case of EnumMeta and Enum, the only reason why Enum.__new__()
is even called is because EnumMeta.__call__()
explicitly calls cls.__new__(cls, value)
.
Additionally, in the case of Enum, A.__new__()
is only called when instantiating the enum instances (one for NULL, one for BOB and one for ALICE), so trying to overload Enum.__new__
using A.__new__
or B.__new__
in the hope of changing B("BOB")
's (failing) behavior is useless. What needs to change to allow B("BOB")
to be valid syntax is the Enum.__new__()
method itself.
What I did was (removing the stdlib comments to make my change more visible):
def __fixed_new(cls, value):
if type(value) is cls:
return value
try:
return cls._value2member_map_[value]
except KeyError:
pass
except TypeError:
for member in cls._member_map_.values():
if member._value_ == value:
return member
try:
exc = None
######
#NB: the following is literally the only line that changes compared to the standard enum.py file
result = cls._missing_(value) if not isinstance(value, str) else cls[value]
######
except Exception as e:
exc = e
result = None
if isinstance(result, cls):
return result
else:
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
if result is None and exc is None:
raise ve_exc
elif exc is None:
exc = TypeError(
'error in %s._missing_: returned %r instead of None or a valid member'
% (cls.__name__, result)
)
exc.__context__ = ve_exc
raise exc
Enum.__new__ = __fixed_new
With this change, any class inherit from enum can "construct new enum instances" (actually, call and get preconstructed enum values) from the corresponding string value, and the original snippet prints:
int: B.BOB
str: B.BOB
Solution 2:[2]
The typical way to look up an enum member by name is with the __getitem__
syntax:
>>> B["BOB"]
<B.BOB: 1>
If you need to be able to do value lookups using the name, you can define _missing_
:
class A(Enum):
#
@classmethod
def _missing_(cls, name):
return cls[name]
class B(A):
BOB = 1
and in use:
>>> B("BOB")
<B.BOB: 1>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Tristan Duquesne |
Solution 2 | Ethan Furman |