I want to write a Python function that takes Callable
objects and corresponding arguments as input, and returns a mapping from the Callable
objects to the values of these objects on the arguments. More concretely, the code might look something like the following.
>>> import collections
>>> import dataclasses
>>> from typing import Iterable, List, Mapping
>>> @dataclasses.dataclass(frozen=True)
... class Adder:
... x: int = 0
... def __call__(self, y: int) -> int:
... return self.x + y
...
>>> def fn_vals(fns: Iterable[Adder], vals: Iterable[int]) -> Mapping[Adder, List[int]]:
... values_from_function = collections.defaultdict(list)
... for fn in fns:
... for val in vals:
... values_from_function[fn].append(fn(val))
... return values_from_function
...
>>> fn_vals((Adder(), Adder(2)), (1, 2, 3))
defaultdict(<class 'list'>, {Adder(x=0): [1, 2, 3], Adder(x=2): [3, 4, 5]})
However, I'm struggling to get this to work with a broader class of Callable
objects. In particular, the following fails with an error saying that __hash__
has not been implemented.
>>> import dataclasses
>>> from typing import Callable, Hashable
>>> class MyFunctionInterface(Callable, Hashable): pass
...
>>> @dataclasses.dataclass(frozen=True)
... class Adder(MyFunctionInterface):
... x: int = 0
... def __call__(self, y: int) -> int:
... return self.x + y
...
>>> Adder()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/alex/anaconda3/lib/python3.7/typing.py", line 814, in __new__
obj = super().__new__(cls)
TypeError: Can't instantiate abstract class Adder with abstract methods __hash__
I'd like to modify my fn_vals
function so that fns
has type Iterable[MyFunctionInterface]
because the only properties that I need the elements of fns
to have is that they be Callable
and Hashable
. Is there a way to indicate that a dataclass satisfies MyFunctionInterface
, and have the __hash__
function still be generated by the dataclass
decorator?
The problem here is a bad interaction between abc
and class decorators.
Quoting the abc
docs,
Dynamically adding abstract methods to a class, or attempting to modify the abstraction status of a method or class once it is created, are not supported.
Once a class is created, you're not allowed to change its abstractness (or at least, it's not allowed on the Python version you're on). Unfortunately, class decorators like dataclasses.dataclass
kick in after the class is already created.
When Adder
is initially created, it doesn't have a __hash__
implementation. abc
inspects the class at this point and determines that the class is abstract. Then, the decorator adds __hash__
and all the other dataclass stuff to the class, but it's already too late.
Your class does have a __hash__
method, but the abc
mechanism doesn't know that.
As of Python 3.10, changing the abstractness status of a class now is allowed, if you call abc.update_abstractmethods
on the class once you're done. The Python 3.10 version of dataclasses
uses this to fix the problem you ran into. You're on 3.7, though. (3.10 wasn't out when this question was asked.)
As for how to proceed, there are three primary options. The first, now that 3.10 is out, is to update Python, but that doesn't work if you have to support versions below 3.9. The second option is to eliminate MyFunctionInterface
entirely and just annotate your callable-hashable objects as Any
. The third is, assuming you want your objects to specifically be callable with a single int argument and return an int, you can define a protocol
class MyProto(typing.Protocol):
def __call__(self, y: int) -> int: ...
def __hash__(self) -> int: ...
and then annotate your objects as MyProto
.