提问人:giuliano-oliveira 提问时间:12/25/2022 最后编辑:giuliano-oliveira 更新时间:2/17/2023 访问量:260
如何只键入 Protocol 方法的第一个位置参数并让其他参数取消类型化?
How to type only the first positional parameter of a Protocol method and let the others be untyped?
问:
问题
如何只键入 Protocol 方法的第一个位置参数并让其他参数取消类型化?
例如,有一个名为的协议,该方法的名称只需要第一个位置参数是 int,而让其余参数是非类型化的。
以下类将正确实现它而不会出错:MyProtocol
my_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
**kwargs
Protocol
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.13
0.991
答:
一种合理的解决方法是使方法仅接受类型化参数,并将非类型化参数留给方法返回的可调用对象。由于可以使用省略号声明可调用对象的返回类型,而无需指定调用签名,因此它解决了将这些附加参数保留为非类型化的问题:
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
如果可以接受任意数量的参数,则不能有一个接受一组数字的子类型(或实现),这打破了 Liskov 替换原则,因为子类型只接受超类型接受的有限一组情况。MyProtocol
[原段]
然后,如果你继续继承 ,你就会继续制作协议,协议与 s 不同,它们使用结构子类型(不是名义子类型),这意味着只要一个对象实现了协议的所有方法/属性,它就是该协议的实例(有关详细信息,请参阅 PEP 544)。Protocol
ABC
[原段完]
[进一步阅读后编辑]
在我看来,协议应该只被其他协议继承,这些协议将与结构子类型一起使用。对于名义子类型(例如允许默认实现),我会使用 ABC。
[进一步阅读后编辑]
如果没有关于您要使用的实现的更多细节,@blhsing的解决方案可能是最开放的,因为它不键入 Callable 的调用签名。
这是一组围绕具有逆变类型的通用协议的实现(绑定为浮点数,因为它是数字塔的顶部),这将允许两个 and 参数的任何数字类型。x
y
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]
评论
方法“Imp1.my_method()”的签名与类“MyProtocol”中基方法的签名不匹配
我想一定是
class Imp1(MyProtocol): def my_method(self, first_param: int, *args: Any, **kwargs: Any) -> int: ...
您的 Imp2 与 Imp1 中的相同,但甚至没有第一个命名参数。
评论
super().__init__()