functools.singledispatchmethod with own class as arg type

Question:

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?

Asked By: Vinzent

||

Answers:

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)

Answered By: juanpa.arrivillaga