如何只键入 Protocol 方法的第一个位置参数并让其他参数取消类型化?

How to type only the first positional parameter of a Protocol method and let the others be untyped?

提问人:giuliano-oliveira 提问时间:12/25/2022 最后编辑:giuliano-oliveira 更新时间:2/17/2023 访问量:260

问:

问题

如何只键入 Protocol 方法的第一个位置参数并让其他参数取消类型化?

例如,有一个名为的协议,该方法的名称只需要第一个位置参数是 int,而让其余参数是非类型化的。 以下类将正确实现它而不会出错:MyProtocolmy_method

class Imp1(MyProtocol):
  def my_method(self, first_param: int, x: float, y: float) -> int:
    return int(first_param - x + y)

但是,以下实现无法正确实现它,因为第一个参数是浮点数:

class Imp2(MyProtocol):
  def my_method(self, x: float, y: float) -> int: # Error, method must have a int parameter as a first argument after self
    return int(x+y)

我以为我可以用 来做到这一点,并结合如下:*args**kwargsProtocol

from typing import Protocol, Any

class MyProtocol(Protocol):
    def my_method(self, first_param: int, /, *args: Any, **kwargs: Any) -> int:
        ...

但是(在 mypy 中)这会使 Imp1 和 Imp2 都失败,因为它强制方法合约真正有一个 ,如下所示:*args**kwargs

class Imp3(MyProtocol):
    def my_method(self, first_param: int, /, *args: Any, **kwargs: Any) -> int:
        return first_param

但这并不能解决我试图实现的目标,即使实现类具有除第一个参数之外的任何类型化/非类型化参数。

解决方法

我设法通过使用带有 setter 的抽象类来规避这个问题,如下所示:set_first_param

from abc import ABC, abstractmethod
from typing import Any


class MyAbstractClass(ABC):
    _first_param: int

    def set_first_param(self, first_param: int):
        self._first_param = first_param

    @abstractmethod
    def my_method(self, *args: Any, **kwargs: Any) -> int:
        ...


class AbcImp1(MyAbstractClass):
    def my_method(self, x: float, y: float) -> int:
        return int(self._first_param + x - y) # now i can access the first_parameter with self._first_param

但这完全改变了我试图实现的初始 API,并且在我看来,对于实现方法来说,在调用之前设置此参数不太清楚。my_method

注意

此示例使用 python 版本和 mypy 版本进行了测试。3.9.130.991

python 类型提示 mypy python-3.9 鸭子类型

评论

5赞 STerliakov 12/26/2022
没办法,幸运的是
0赞 J_H 12/30/2022
你不是在打电话和类似,但你应该是。另外,也许您可以在运行时使用 isinstance 检查 args,并在违反合约时发出错误信号?super().__init__()

答:

1赞 blhsing 12/30/2022 #1

一种合理的解决方法是使方法仅接受类型化参数,并将非类型化参数留给方法返回的可调用对象。由于可以使用省略号声明可调用对象的返回类型,而无需指定调用签名,因此它解决了将这些附加参数保留为非类型化的问题:

from typing import Protocol, Callable

class MyProtocol(Protocol):
    def my_method(self, first_param: int) -> Callable[..., int]:
        ...

class Imp1(MyProtocol):
  def my_method(self, first_param: int) -> Callable[..., int]:
      def _my_method(x: float, y: float) -> int:
          return int(first_param - x + y)
      return _my_method

print(Imp1().my_method(5)(1.5, 2.5)) # outputs 6

传递mypy的代码演示:

https://mypy-play.net/?mypy=latest&python=3.12&gist=677569f73f6fc3bc6e44858ef37e9faf

1赞 ljmc 1/3/2023 #2

如果可以接受任意数量的参数,则不能有一个接受一组数字的子类型(或实现),这打破了 Liskov 替换原则,因为子类型只接受超类型接受的有限一组情况。MyProtocol

[原段]

然后,如果你继续继承 ,你就会继续制作协议,协议与 s 不同,它们使用结构子类型(不是名义子类型),这意味着只要一个对象实现了协议的所有方法/属性,它就是该协议的实例(有关详细信息,请参阅 PEP 544)。ProtocolABC

[原段完]

[进一步阅读后编辑]

在我看来,协议应该只被其他协议继承,这些协议将与结构子类型一起使用。对于名义子类型(例如允许默认实现),我会使用 ABC。

[进一步阅读后编辑]

如果没有关于您要使用的实现的更多细节,@blhsing的解决方案可能是最开放的,因为它不键入 Callable 的调用签名。

这是一组围绕具有逆变类型的通用协议的实现(绑定为浮点数,因为它是数字塔的顶部),这将允许两个 and 参数的任何数字类型。xy

from typing import Any, Generic, Protocol, TypeVar

T = TypeVar("T", contravariant=True, bound=float)
U = TypeVar("U", contravariant=True, bound=float)

class MyProtocol(Protocol[T, U]):
    def my_method(self, first_param: int, x: T, y: U) -> int:
        ...

class ImplementMyProtocol1(Generic[T, U]):
    """Generic implementation, needs typing"""
    def my_method(self, first_param: int, x: T, y: U) -> int:
        return int(first_param - x + y)

class ImplementMyProtocol2:
    """Float implementation, and ignores first argument"""
    def my_method(self, _: int, x: float, y: float) -> int:
        return int(x + y)

class ImplementMyProtocol3:
    """Another float implementation, with and extension"""
    def my_method(self, first_param: int, x: float, y: float, *args: float) -> int:
        return int(first_param - x + y + sum(args))

def use_MyProtocol(inst: MyProtocol[T, U], n: int, x: T, y: U) -> int:
    return inst.my_method(n, x, y)

use_MyProtocol(ImplementMyProtocol1[float, float](), 1, 2.0, 3.0)  # OK MyProtocol[float, float]
use_MyProtocol(ImplementMyProtocol1[int, int](), 1, 2, 3)  # OK MyProtocol[int, int]
use_MyProtocol(ImplementMyProtocol2(), 1, 2.0, 3.0)  # OK MyProtocol[float, float]
use_MyProtocol(ImplementMyProtocol3(), 1, 2.0, 3.0)  # OK MyProtocol[float, float]

评论

0赞 giuliano-oliveira 1/4/2023
谢谢你的解释!我最终做了类似的事情,但我没有为每个额外的参数使用泛型类型,而是使用了一个可以是数据类的泛型类型:gist.github.com/giuliano-oliveira/...
1赞 ljmc 1/4/2023
我看到您在实现中仍然使用该协议作为父协议,这很可能是错误的。当你的实现继承自你的协议时,它就变成了一个协议本身,而协议本质上不是一个实现。协议使用结构子类型而不是名义子类型,即它们不需要继承
0赞 giuliano-oliveira 1/5/2023
我明白了,这是有道理的,但我基本上是在使用 mypy 建议的,当您想在实例化子类时键入检查子类时。“显式包含协议作为基类也是记录您的类实现特定协议的一种方式” PEP 544 和 mypy 之间是否存在某种不匹配?mypy.readthedocs.io/en/stable/......
1赞 ljmc 1/5/2023
好的,我进一步看了一下,我必须说我错了,你确实可以显式继承一个协议,它允许默认实现等。我宁愿拥有 ABC 的名义继承和默认实现,以及接口的协议,但这是个人选择。
0赞 Ronin 1/4/2023 #3
  1. 方法“Imp1.my_method()”的签名与类“MyProtocol”中基方法的签名不匹配

    我想一定是

     class Imp1(MyProtocol):
         def my_method(self, first_param: int, *args: Any, **kwargs: Any) -> int:
             ...
    
  2. 您的 Imp2 与 Imp1 中的相同,但甚至没有第一个命名参数。