I would like to use functools.singledispatchmethod
to overload the binary arithmetic operator methods of a class called Polynomial
. The problem I have is that I can't find a way to register method calls where other
is a Polynomial
.
Perhaps better explained with a simple example:
from __future__ import annotations
from functools import singledispatchmethod
class Polynomial:
@singledispatchmethod
def __add__(self, other):
return NotImplemented
@__add__.register
def _(self, other: Polynomial):
return NotImplemented
The code above raises a NameError:
NameError: name 'Polynomial' is not defined
This NameError
is not caused by the annotation but is raised inside functools
. Also, annotating using a string 'Polynomial'
instead without the need of __future__.annotations
doesn't work either. This behavior is not documented in the documentation for functools.singledispatchmethod.
I can make it work by making Polynomial
inherit from another class, and then using that class in the type annotation:
from functools import singledispatchmethod
class _Polynomial:
pass
class Polynomial(_Polynomial):
@singledispatchmethod
def __add__(self, other):
return NotImplemented
@__add__.register
def _(self, other: _Polynomial):
return NotImplemented
..but I am not overly fond of this solution.
How can I make this work without needing the useless intermediate class?
The alternative is to add the methods after the class definition:
from __future__ import annotations
from functools import singledispatchmethod
class Polynomial:
pass
@singledispatchmethod
def __add__(self, other):
return NotImplemented
@__add__.register
def _(self, other: Polynomial):
return NotImplemented
Polynomial.__add__ = __add__
You can't actually reference the class during a class definition, because the class doesn't exist yet. Using the from __future__ import annotations
makes annotations get saved as strings, but as soon as the decorator tries to evaluate those strings for their values, the same problem occurs, so it doesn't get around it (because it isn't a mere annotation).
This is evident in the stack trace (abbreviated):
~/miniconda3/envs/py39/lib/python3.9/functools.py in register(cls, func)
858 # only import typing if annotation parsing is necessary
859 from typing import get_type_hints
--> 860 argname, cls = next(iter(get_type_hints(func).items()))
861 if not isinstance(cls, type):
862 raise TypeError(
~/miniconda3/envs/py39/lib/python3.9/typing.py in get_type_hints(obj, globalns, localns, include_extras)
1447 if isinstance(value, str):
1448 value = ForwardRef(value)
-> 1449 value = _eval_type(value, globalns, localns)
1450 if name in defaults and defaults[name] is None:
1451 value = Optional[value]
~/miniconda3/envs/py39/lib/python3.9/typing.py in _eval_type(t, globalns, localns, recursive_guard)
281 """
282 if isinstance(t, ForwardRef):
--> 283 return t._evaluate(globalns, localns, recursive_guard)
284 if isinstance(t, (_GenericAlias, GenericAlias)):
285 ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__)
~/miniconda3/envs/py39/lib/python3.9/typing.py in _evaluate(self, globalns, localns, recursive_guard)
537 localns = globalns
538 type_ =_type_check(
--> 539 eval(self.__forward_code__, globalns, localns),
540 "Forward references must evaluate to types.",
541 is_argument=self.__forward_is_argument__,
So, when the singledispatchmethod
decorater typing.get_type_hints
, which is essentially eval
(with the added feature of taking care to obtain the correct global scope to evaluate the string in, the one where the annotation was made)