使用常规方法和类方法实现 Python 协议



假设我有两个类Foo1Foo2来实现方法bar()

  • Foo1中,bar()是一个常规方法
  • Foo2bar()是一个@classmethod
class Foo1:
def bar(self) -> None:
print("foo1.bar")

class Foo2:
@classmethod
def bar(cls) -> None:
print("Foo2.bar")

现在假设我有一个函数,它接受"任何具有bar()方法"的列表并调用它:

def foreach_foo_call_bar(foos):
for foo in foos:
foo.bar()

在运行时调用此函数可以正常工作:

foreach_foo_call_bar([Foo1(), Foo2])

因为Foo1()Foo2都有bar()方法。

但是,如何正确向foreach_foo_call_bar()添加类型提示?

我尝试创建一个名为SupportsBar的 PEP544Protocol

class SupportsBar(Protocol):
def bar(self) -> None:
pass

并像这样注释:

def foreach_foo_call_bar(foos: Iterable[SupportsBar]):
...

但mypy说:

List item 1 has incompatible type "Type[Foo2]"; expected "SupportsBar"

知道如何正确注释吗?

问题似乎是Protocol专门检查是否支持实例方法,而不仅仅是是否存在正确名称的属性。在Foo2的情况下,这意味着元类需要一个名为bar的实例方法;以下内容似乎按预期和类型检查运行。

# Define a metaclass that provides an instance method bar for its instances.
# A metaclass instance method is almost equivalent to a class method.
class Barrable(type):
def bar(cls) -> None:
print(cls.__name__ + ".bar")

class Foo1:
def bar(self) -> None:
print("foo1.bar")

class Foo2(metaclass=Barrable):
pass

class SupportsBar(Protocol):
def bar(self) -> None:
pass

def foreach_foo_call_bar(foos: Iterable[SupportsBar]):
for foo in foos:
foo.bar()

我不会声称将类方法转换为元类实例方法是一个很好的解决方法(实际上,它对静态方法没有任何作用(,但它指出这是Protocol它不处理任意属性的基本限制。

这是mypy的一个开放问题。 BFDL自己甚至在@MatanRubin提出这个问题的2年前就承认这是不正确的行为。 它仍未解决,但最近(2019 年 11 月(被标记为高优先级,因此希望此处提供的示例不会很快产生误报。

foos列表中的第二项,Foo2是一个类而不是像Foo1()这样的实例。这需要通过环绕Type来指定:

def foreach_foo_call_bar(
foos: Iterable[Union[SupportsBar,
Type[SupportsBar]]]):
# ...

您也可以在不使用Protocol的情况下执行此操作:

def foreach_foo_call_bar(
foos: Iterable[Union[Foo1,
Type[Foo2]]]):
# ...

最新更新