如何创建一个包含 threading.Event 和 multiprocessing.Event 的协议?

sag*_*ian 3 python type-hinting mypy

Python 标准库multiprocessing.Event被明确声明为克隆threading.Event并具有相同的接口。我想注释变量和参数,以便它们可以接受这些类中的任何一个,并对mypy它们进行类型检查。我尝试创建一个协议(我使用了它,multiprocessing.synchronize.Event因为这是由 返回的实际类multiprocessing.Event)。

import multiprocessing
import threading

from typing import Optional, Type, Protocol


class Event(Protocol):
    def wait(self, timeout: Optional[float]) -> bool:
        ...

    def set(self) -> None:
        ...

    def clear(self) -> None:
        ...

    def is_set(self) -> bool:
        ...


class Base:
    flag_class: Type[Event]

    def foo(self, e: Event):
        pass


class DerivedOne(Base):
    flag_class = multiprocessing.synchronize.Event

    def foo(self, e: multiprocessing.synchronize.Event):
        pass


class DerivedTwo(Base):
    flag_class = threading.Event

    def foo(self, e: threading.Event):
        pass
Run Code Online (Sandbox Code Playgroud)

然而,mypy(版本 0.761)无法识别这一点multiprocessing.Event,并且threading.Event两者都实现了我定义的协议:

$ mypy proto.py

proto.py:31: error: Argument 1 of "foo" is incompatible with supertype "Base"; supertype defines the argument type as "Event"
proto.py:38: error: Argument 1 of "foo" is incompatible with supertype "Base"; supertype defines the argument type as "Event"
Found 2 errors in 1 file (checked 1 source file)
Run Code Online (Sandbox Code Playgroud)

为什么无法mypy识别我的协议以及如何修复它?

Mar*_*ers 5

这不是Protocol问题。您将 的签名更改为foo严格的变体Base.foo()接受任何 Event实现,而每个子类只接受一个具体实现。这违反了里氏替换原则,而 Mypy 不允许这样做是正确的。

您必须使用绑定TypeVarGeneric此处的组合,以便您可以创建Base采用不同类型的不同具体子类:

from typing import Generic, Optional, Protocol, Type, TypeVar

# Only things that implement Event will do
T = TypeVar("T", bound=Event)

# Base is Generic, subclasses need to state what exact class
# they use for T; as long as it's an Event implementation, that is.
class Base(Generic[T]):
    flag_class: Type[T]

    def foo(self, e: T) -> None:
        pass
Run Code Online (Sandbox Code Playgroud)

这本质上是Base一种模板类,T只要任何东西实现了你的协议,你就可以在模板槽中插入任何东西。这也更加健壮,因为您现在不会意外地混淆实现Event(将threading.Event和组合multiprocessing.Event在单个子类中)。

因此,以下两种不同Event实现的子类是正确的:

class DerivedOne(Base[multiprocessing.synchronize.Event]):
    flag_class = multiprocessing.synchronize.Event

    def foo(self, e: multiprocessing.synchronize.Event) -> None:
        pass

class DerivedTwo(Base[threading.Event]):
    flag_class = threading.Event

    def foo(self, e: threading.Event) -> None:
        pass
Run Code Online (Sandbox Code Playgroud)

但使用未实现协议方法的类是错误的:

# Mypy flags the following class definition as an error, because a lock
# does not implement the methods of an event.
# error: Type argument "threading.Lock" of "Base" must be a subtype of "proto.Event"
class Wrong(Base[threading.Lock]):
    flag_class = threading.Lock

    def foo(self, e: threading.Lock) -> None:
        pass
Run Code Online (Sandbox Code Playgroud)

混合类型也是一个错误:

# Mypy flags 'def foo' as an error because the type it accepts differs from
# the declared type of the Base[...] subclass
# error: Argument 1 of "foo" is incompatible with supertype "Base"; supertype defines the argument type as "Event"
class AlsoWrong(Base[threading.Event]):
    flag_class = threading.Event

    def foo(self, e: multiprocessing.synchronize.Event) -> None:
        pass

# Mypy flags 'flag_class' as an error because the type differs from the
# declared type of the Base[...] subclass
# error: Incompatible types in assignment (expression has type "Type[multiprocessing.synchronize.Event]", base class "Base" defined the type as "Type[threading.Event]")
class StillWrong(Base[threading.Event]):
    flag_class = multiprocessing.synchronize.Event

    def foo(self, e: threading.Event) -> None:
        pass
Run Code Online (Sandbox Code Playgroud)