4

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?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Vinzent
  • 1,070
  • 1
  • 9
  • 14

1 Answers1

2

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)

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Awesome thx! I actually tried to do that in a couple of different ways but I couldn't get it to work, mainly because I thought that `@singledispatchmethod` had to be applied to a method defined in the class body. – Vinzent Aug 07 '22 at 20:20
  • @Vinzent so, methods don't have to be define in a class body usually. Only if, perhaps, if there is some metaclass magic going on – juanpa.arrivillaga Aug 07 '22 at 20:22
  • 1
    I know that I just thought that perhaps `singledispatchmethod` needed access to the class that the method was defined on, but apparently it doesn't. – Vinzent Aug 07 '22 at 20:24
  • @Vinzent indeed, *it can't have access to it in the class body!* – juanpa.arrivillaga Aug 07 '22 at 20:25
  • 1
    No you're right, thinking more about it I realize that is obviously true. – Vinzent Aug 07 '22 at 20:27
  • Actually as I just discovered you can save a line of code by defining the `__add__` function inside the class body and then decorating the overload with `@Polynomial.__add__.register`, that way you avoid the line `Polynomial.__add__ = __add__`. This seems a bit cleaner to me and also has the advantage that you avoid having the `__add__` in the global scope and so if you have multiple classes you need to do this for (which I do) then you don't have to come up with different function names for the additions for each of them. – Vinzent Aug 07 '22 at 21:00
  • I think this is a shame and limits the usefulness of `singledispatchmethod`. It would be great if Python found a way around that problem. – Darkdragon84 May 22 '23 at 08:40