python 如何只输入Protocol方法的第一个位置参数,而不输入其他参数?

vwkv1x7d  于 2023-01-04  发布在  Python
关注(0)|答案(2)|浏览(148)
    • bounty将在16小时后过期**。回答此问题可获得+50的声望奖励。giuliano-oliveira正在寻找规范答案

问题

如何只输入Protocol方法的第一个位置参数,而不输入其他参数?
例如,有一个名为MyProtocol的协议,该协议有一个名为my_method的方法,该方法只要求第一个位置参数为int,而其余参数不需要类型化。下面的类将正确地实现它,没有错误:

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版本3.9.13和mypy版本0.991测试的。

vngu2lb8

vngu2lb81#

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

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

eanckbw9

eanckbw92#

如果你的MyProtocol可以接受任意数量的参数,你就不能有一个接受一组数量的子类型(或实现),这就破坏了Liskov substitution principle,因为子类型只接受父类型所接受的有限的一组情况。
然后,如果你继续从Protocol继承,你继续制定协议,协议不同于ABC,它们使用结构化子类型(而不是名义子类型),这意味着只要一个对象实现了协议的所有方法/属性,它就是该协议的示例(更多细节请参见PEP 544)。
如果没有关于您想要使用的实现的更多细节,@blhsing的解决方案可能是最开放的,因为它不输入Callable的调用签名。
下面是一组围绕具有逆变类型(绑定为float,因为它是数字塔的顶部)的通用协议的实现,这将允许两个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]

相关问题