在以下代码中
some_method
已由元类添加:
from abc import ABC
from abc import ABCMeta
from typing import Type
def some_method(cls, x: str) -> str:
return f"result {x}"
class MyMeta(ABCMeta):
def __new__(mcs, *args, **kwargs):
cls = super().__new__(mcs, *args, **kwargs)
cls.some_method = classmethod(some_method)
return cls
class MyABC(ABC):
@classmethod
def some_method(cls, x: str) -> str:
return x
class MyClassWithSomeMethod(metaclass=MyMeta):
pass
def call_some_method(cls: Type[MyClassWithSomeMethod]) -> str:
return cls.some_method("A")
if __name__ == "__main__":
mc = MyClassWithSomeMethod()
assert isinstance(mc, MyClassWithSomeMethod)
assert call_some_method(MyClassWithSomeMethod) == "result A"
然而,MyPy 是相当令人期待的对此不满意:
minimal_example.py:27: error: "Type[MyClassWithSomeMethod]" has no attribute "some_method"
Found 1 error in 1 file (checked 1 source file)
有没有什么优雅的方法来告诉类型检查器,类型真的没问题?我所说的优雅,是指我不需要到处改变这些定义:
class MyClassWithSomeMethod(metaclass=MyMeta): ...
注意,我不想进行子类化(就像上面代码中的
MyABC
一样)。也就是说,我的类将用 metaclass=
定义。
有哪些选择?
我也尝试过
Protocol
:
from typing import Protocol
class SupportsSomeMethod(Protocol):
@classmethod
def some_method(cls, x: str) -> str:
...
class MyClassWithSomeMethod(SupportsSomeMethod, metaclass=MyMeta):
pass
def call_some_method(cls: SupportsSomeMethod) -> str:
return cls.some_method("A")
但这会导致:
类型错误:元类冲突:派生类的元类必须是其所有基类的元类的(非严格)子类
正如 MyPy 文档中所解释的,MyPy 对元类的支持仅限于此:
Mypy 不会也无法理解任意元类代码。
问题是,如果您将一个方法猴子修补到元类的
__new__
方法中的类上,则可能会在类的定义中添加 anything。这对 Mypy 来说太动态了,无法理解。
但是,一切并没有失去!您在这里有几个选择。
类是其元类的实例,因此元类上的实例方法与类中定义的classmethod
非常相似。因此,您可以按如下方式重写
minimal_example.py
,MyPy会很高兴:
from abc import ABCMeta
from typing import Type
class MyMeta(ABCMeta):
def some_method(cls, x: str) -> str:
return f"result {x}"
class MyClassWithSomeMethod(metaclass=MyMeta):
pass
def call_some_method(cls: Type[MyClassWithSomeMethod]) -> str:
return cls.some_method("A")
if __name__ == "__main__":
mc = MyClassWithSomeMethod()
assert isinstance(mc, MyClassWithSomeMethod)
assert call_some_method(MyClassWithSomeMethod) == "result A"
元类实例方法与平均方法之间唯一的大区别 classmethod
是元类实例方法不能从使用元类的类实例中获得:
>>> from abc import ABCMeta
>>> class MyMeta(ABCMeta):
... def some_method(cls, x: str) -> str:
... return f"result {x}"
...
>>> class MyClassWithSomeMethod(metaclass=MyMeta):
... pass
...
>>> MyClassWithSomeMethod.some_method('foo')
'result foo'
>>> m = MyClassWithSomeMethod()
>>> m.some_method('foo')
Traceback (most recent call last):
File "<string>", line 1, in <module>
AttributeError: 'MyClassWithSomeMethod' object has no attribute 'some_method'
>>> type(m).some_method('foo')
'result foo'
在这些情况下,另一种选择是“承诺”MyPy 存在一个方法,而不实际定义它。您可以使用标准注释语法来完成此操作:
from abc import ABCMeta
from typing import Type, Callable
def some_method(cls, x: str) -> str:
return f"result {x}"
class MyMeta(ABCMeta):
some_method: Callable[['MyMeta', str], str]
def __new__(mcs, *args, **kwargs):
cls = super().__new__(mcs, *args, **kwargs)
cls.some_method = classmethod(some_method)
return cls
class MyClassWithSomeMethod(metaclass=MyMeta):
pass
def call_some_method(cls: Type[MyClassWithSomeMethod]) -> str:
return cls.some_method("A")
if __name__ == "__main__":
mc = MyClassWithSomeMethod()
assert isinstance(mc, MyClassWithSomeMethod)
assert call_some_method(MyClassWithSomeMethod) == "result A"
这通过了 MyPy 很好,而且实际上相当干净。但是,这种方法存在局限性,因为可调用的全部复杂性无法使用简写 typing.Callable
语法来表达。选项 3:对 MyPy 撒谎
选项 3(a)。使用 typing.TYPE_CHECKING
常量对 MyPy 撒谎 对于静态类型检查器,
typing.TYPE_CHECKING
常量始终为
True
,并且在运行时始终为
False
。因此,您可以使用此常量将类的不同定义提供给 MyPy,而不是在运行时使用的定义。
from typing import Type, TYPE_CHECKING
from abc import ABCMeta
if not TYPE_CHECKING:
def some_method(cls, x: str) -> str:
return f"result {x}"
class MyMeta(ABCMeta):
if TYPE_CHECKING:
def some_method(cls, x: str) -> str: ...
else:
def __new__(mcs, *args, **kwargs):
cls = super().__new__(mcs, *args, **kwargs)
cls.some_method = classmethod(some_method)
return cls
class MyClassWithSomeMethod(metaclass=MyMeta):
pass
def call_some_method(cls: Type[MyClassWithSomeMethod]) -> str:
return cls.some_method("A")
if __name__ == "__main__":
mc = MyClassWithSomeMethod()
assert isinstance(mc, MyClassWithSomeMethod)
assert call_some_method(MyClassWithSomeMethod) == "result A"
这通过了MyPy。这种方法的主要缺点是在代码库中进行 if TYPE_CHECKING
检查实在是太难看了。
选项 3(b):使用 .pyi
存根文件对 MyPy 撒谎另一种欺骗 MyPy 的方法是使用
.pyi
存根文件。你可以有一个像这样的
minimal_example.py
文件:
from abc import ABCMeta
def some_method(cls, x: str) -> str:
return f"result {x}"
class MyMeta(ABCMeta):
def __new__(mcs, *args, **kwargs):
cls = super().__new__(mcs, *args, **kwargs)
cls.some_method = classmethod(some_method)
return cls
并且您可以在同一目录中拥有一个 minimal_example.pyi
存根文件,如下所示:
from abc import ABCMeta
class MyMeta(ABCMeta):
def some_method(cls, x: str) -> str: ...
如果 MyPy 在同一目录中找到 .py
文件和
.pyi
文件,它将始终忽略
.py
文件中的定义,而使用
.pyi
文件中的存根。同时,在运行时,Python 执行相反的操作,完全忽略
.pyi
文件中的存根,而完全支持
.py
文件中的运行时实现。因此,您可以在运行时随心所欲地动态,而 MyPy 则不会更明智。(如您所见,无需在
.pyi
文件中复制完整的方法定义。MyPy 只需要这些方法的签名,因此约定只是将函数的主体填充到
.pyi
中带有文字省略号的文件
...
。)此解决方案比使用
TYPE_CHECKING
常数更干净。然而,我不会因为使用
.pyi
文件而得意忘形。尽可能少地使用它们。如果您的
.py
文件中有一个类,而您的存根文件中没有该类的副本,则 MyPy 将完全不知道它的存在,并引发各种误报错误。请记住:如果您有一个
.pyi
文件,MyPy 将完全忽略其中包含运行时实现的
.py
文件。在
.pyi
文件中复制类定义不利于 DRY,并且存在更新
.py
文件中的运行时定义但忘记更新
.pyi
文件的风险。如果可能,您应该将“真正需要”的代码隔离到一个单独的“存根”中,并将其放入一个短文件中。 然后,您应该在项目的其余部分中像往常一样注释类型,并在代码的其余部分需要时像往常一样从
.pyi
导入必要的类。