Type hinting callback functions
Hello, I've tried everything I can think of to get the following to pass mypy to no avail. ``` from abc import ABCMeta from typing import Callable, ClassVar, List class Base(metaclass=ABCMeta): # a list of functions taking an instance of a `Base` subclass callbacks: ClassVar[List[Callable[['Base'], bool]]] = [] def run(self) -> None: for cb in self.callbacks: cb(self) # multiple, independent classes implementing `Base` class A(Base): def meth1(self) -> bool: return True callbacks = [meth1] class B(Base): def meth2(self) -> bool: return True callbacks = [meth2] ``` Is this possible to type hint without annotating each and every unbound method used as a callback? I would like to be able to assert that the callbacks do, in fact, take `Base`-derived instances as their arguments. I would have thought that this should Just Work, like in "normal" statically-typed languages. Thanks, Jeremy
The reason this doesn't work is that the instance methods `A.meth1` and `B.meth2` expect `self` to be an instance of their own class (or a subtype thereof). For example, the `self` in `A` is implicitly typed as `A`. The full type of this method is `Callable[[A], bool]`. You are then trying to assign this to a `Callable[[Base], bool]`. Parameter types within a callable are contravariant, so this is a type violation. This makes logical sense if you consider that the callback could be invoked with an instance of `Base` (but not `A`) as the first argument. This would violate the assumption made by `A.meth1` is expecting the caller to pass an instance of `A`. If you explicitly annotate the `self` parameter in `A.meth1` and `B.meth2` to accept an instance of `Base`, you can eliminate the type violation. ```python def meth1(self: Base) -> bool: ... ``` -- Eric Traut Contributor to Pyright & Pylance Microsoft Corp.
As @Eric states, there is a real type violation in this design. However, the suggested trick is not a good idea: claiming that `A.meth1` would accept any `Base` object is a lie, unless `meth1` would only use `Base` members, in which case it could as well have been defined in the `Base` class itself. The Liskov Substitution Principle is at play here: the type `Base.Callback` could be defined as `Callable[['Base'], bool]`. When you would fake that `A.meth1` would conform to that signature, it would mean that you're allowed to call `A.meth1(an_instance_of_B)`, since `an_instance_of_B` is also an instance of `Base`. What you _really_ seem to mean is to create a collection of `Subclass.Callback` functions. But since the subclass is not known when you're specifying the Base, youl cannot know its type. To do this, you would need an extra type parameter that would 'curiously recur' in the Subclass declaration, but I'm not sure if this would work. ``` from typing import Callable, Iterable, Generic, TypeVar TSub = TypeVar('TSub') class BaseT(Generic[TSub]): callbacks: Iterable[Callable[[TSub], bool]] = tuple() def fmap(self: TSub): return tuple(cb(self) for cb in self.callbacks) class A(BaseT['A']): def meth1(self: 'A'): return True def meth2(self: 'A'): return False A.callbacks = (A.meth1, A.meth2) a = A() print(a.fmap()) # (True, False) ``` BUT template.py:10: error: "TSub" has no attribute "callbacks" template.py:22: error: Access to generic instance variables via class is ambiguous I'm not sure if Python's Generics are up to speed yet. On Tue, Mar 2, 2021 at 9:38 PM Eric Traut <eric@traut.com> wrote:
The reason this doesn't work is that the instance methods `A.meth1` and `B.meth2` expect `self` to be an instance of their own class (or a subtype thereof). For example, the `self` in `A` is implicitly typed as `A`. The full type of this method is `Callable[[A], bool]`. You are then trying to assign this to a `Callable[[Base], bool]`. Parameter types within a callable are contravariant, so this is a type violation. This makes logical sense if you consider that the callback could be invoked with an instance of `Base` (but not `A`) as the first argument. This would violate the assumption made by `A.meth1` is expecting the caller to pass an instance of `A`.
If you explicitly annotate the `self` parameter in `A.meth1` and `B.meth2` to accept an instance of `Base`, you can eliminate the type violation.
```python def meth1(self: Base) -> bool: ... ```
-- Eric Traut Contributor to Pyright & Pylance Microsoft Corp. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: kristoffel.pirard@gmail.com
participants (3)
-
Eric Traut
-
Jeremy Kloth
-
Kristoffel Pirard