Как совместить пользовательский протокол с протоколом Callable?

У меня есть декоратор, который принимает функцию и возвращает ту же функцию с некоторыми добавленными атрибутами:

import functools
from typing import *


def decorator(func: Callable) -> Callable:
    func.attr1 = "spam"
    func.attr2 = "eggs"
    return func

Как указать возвращаемое значение decorator? Я хочу, чтобы подсказка типа передала две части информации:

  1. возвращаемое значение - Callable
  2. возвращаемое значение имеет атрибуты attr1 и attr2

Если я напишу протокол,

class CallableWithAttrs(Protocol):
    attr1: str
    attr2: str

то я теряю Callable. И, видимо, я не могу заставить протокол наследоваться от Callable;

class CallableWithAttrs(Callable, Protocol):
    attr1: str
    attr2: str

mypy говорит:

error: Invalid base class "Callable"

С другой стороны, если я просто использую Callable, я теряю информацию о добавленных атрибутах.



Это, возможно, еще сложнее при введении переменных типа, т.е. когда декоратор должен возвращать тот же тип вызываемого объекта, что и данная функция func, как указал MisterMiyagi в комментариях.

import functools
from typing import *

C = TypeVar('C', bound=Callable)


def decorator(func: C) -> C:
    func.attr1 = "spam"
    func.attr2 = "eggs"
    return func

Что мне теперь делать? Я не могу наследовать от переменной типа:

class CallableWithAttrs(C, Protocol):
    attr1: str
    attr2: str
error: Invalid base class "C"

person Anakhand    schedule 30.06.2020    source источник
comment
@deceze Больше похоже на Intersection[CallableWithAttrs, Callable] (которого не существует).   -  person Anakhand    schedule 30.06.2020
comment
Да, правда на самом деле.   -  person deceze♦    schedule 30.06.2020
comment
С точки зрения вашего decorator, какое значение имеет то, что func является Callable? Он никогда не называет это так, после утиного ввода я не вижу причин для искусственного ограничения его области действия только Callable объектами (?)   -  person PiCTo    schedule 30.06.2020
comment
@PiCTo Это может не иметь значения для декоратора синтаксически, но имеет значение семантически, то есть в соответствии со спецификацией декоратора. Этот декоратор следует использовать только с Callables. (Приведенный здесь пример является лишь минимальным примером.)   -  person Anakhand    schedule 30.06.2020
comment
Вы хотите вернуть некоторые Callable с атрибутами или func: Callable? То есть, вам нужно сохранить подпись?   -  person MisterMiyagi    schedule 30.06.2020
comment
@MisterMiyagi Хороший вопрос. Желательно второе, но если нет возможности, то и первое тоже подойдет. Я добавил правку по этому поводу.   -  person Anakhand    schedule 30.06.2020


Ответы (2)


Можно параметрировать Protocol с помощью Callable:

from typing import Callable, TypeVar, Protocol

C = TypeVar('C', bound=Callable)  # placeholder for any Callable


class CallableObj(Protocol[C]):   # Protocol is parameterised by Callable C ...
    attr1: str
    attr2: str

    __call__: C                   # ... which defines the signature of the protocol

Это создает пересечение самого Protocol с произвольным Callable.


Таким образом, функция, которая принимает любой вызываемый объект C, может вернуть CallableObj[C], вызываемый объект той же сигнатуры с нужными атрибутами:

def decorator(func: C) -> CallableObj[C]: ...

MyPy правильно распознает как подпись, так и атрибуты:

def dummy(arg: str) -> int: ...

reveal_type(decorator(dummy))           # CallableObj[def (arg: builtins.str) -> builtins.int]'
reveal_type(decorator(dummy)('Hello'))  # int
reveal_type(decorator(dummy).attr1)     # str
decorator(dummy)(b'Fail')  # error: Argument 1 to "dummy" has incompatible type "bytes"; expected "str"
decorator(dummy).attr3     # error: "CallableObj[Callable[[str], int]]" has no attribute "attr3"; maybe "attr2"?
person MisterMiyagi    schedule 30.06.2020

Поскольку typing.Callable соответствует collections.abc.Callable вы можете просто определить Protocol, реализующий __call__:

class CallableWithAttrs(Protocol):
    attr1: str
    attr2: str

    def __call__(self, *args, **kwargs): pass
person a_guest    schedule 30.06.2020
comment
Я пробовал создавать подклассы других typing протоколов, таких как Sized, и mypy не жаловался. Есть ли причина, по которой Callable является исключением? - person Anakhand; 30.06.2020
comment
@Anakhand Обратите внимание, что Callable во многих отношениях имеет особый регистр. Самое главное, это единственный дженерик, который принимает [[Args, ...], Ret], тогда как все остальные Generic просто принимают [Ts, ...]. - person MisterMiyagi; 30.06.2020
comment
@Anakhand Я не совсем уверен, но думаю, это потому, что они предпочитают протоколы обратного вызова, которые будут использоваться. - person a_guest; 30.06.2020