如何使用 metaclass 在 Python 中插入的方法对 class 进行类型检查?
How to typecheck class with method inserted by metaclass in Python?
在以下代码中 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还是挺符合预期的unhappy about it:
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")
但这会导致:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
如 the MyPy documentation 中所述,MyPy 对 metaclasses 的支持仅到此为止:
Mypy does not and cannot understand arbitrary metaclass code.
问题是,如果您将方法猴子修补到元class 的__new__
方法中的class,您可能会添加任何东西 到您 class 的定义。这对于 Mypy 来说太动态了,无法理解。
然而,并非一无所有!您在这里有几个选择。
选项 1:将方法静态定义为元上的实例方法class
类 是其元class 的实例,因此元class to classmethod
s defined in a class. As such, you can rewrite minimal_example.py
as follows, and MyPy will be happy:
上的实例方法
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"
metaclass 实例方法和您的平均 classmethod
之间的唯一大区别是 metaclass 实例方法不能从 class 使用元class:
>>> 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'
选项 2:向 MyPy 承诺一个方法存在,但实际上没有定义它
在很多情况下,您将使用元class,因为您希望比静态定义方法时更动态。例如,您可能希望即时动态生成方法定义并将它们添加到使用元class 的classes。在这些情况下,选项 1 根本行不通。
在这些情况下,另一种选择是“承诺”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"
这 passes MyPy 很好,实际上相当干净。但是,这种方法存在局限性,因为无法使用 shorthand typing.Callable
语法表达可调用对象的全部复杂性。
选项 3:欺骗 MyPy
第三种选择是欺骗 MyPy。有两种明显的方法可以做到这一点。
选项 3(a)。使用 typing.TYPE_CHECKING
常量
欺骗 MyPy
对于静态类型检查程序,typing.TYPE_CHECKING
常量始终为 True
,在运行时始终为 False
。因此,您可以使用此常量将 class 的不同定义提供给 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"
这个passes 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 将 none 变得更聪明。
(如您所见,无需在 .pyi
文件中复制完整的方法定义。MyPy 只需要这些方法的签名,因此约定只是简单地填充一个.pyi
文件中的函数,带有文字省略号 ...
。)
此解决方案比使用 TYPE_CHECKING
常量更简洁。但是,我 不会 使用 .pyi
文件。尽可能少地使用它们。如果你的 .py
文件中有一个 class 而你的存根文件中没有它的副本,MyPy 将完全不知道它的存在并引发各种误报错误。请记住:如果您有一个 .pyi
文件,MyPy 将 完全忽略 包含您的运行时实现的 .py
文件。
在 .pyi
文件中复制 class 定义不利于 DRY,并冒着更新 .py
文件中的运行时定义但忘记更新 [=26] 的风险=] 文件。如果可能,您应该将 真正需要 一个单独的 .pyi
存根的代码隔离到一个短文件中。然后,您应该在项目的其余部分照常注释类型,并在其余代码需要时照常从 very_dynamic_classes.py
导入必要的 classes。
在以下代码中 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还是挺符合预期的unhappy about it:
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")
但这会导致:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
如 the MyPy documentation 中所述,MyPy 对 metaclasses 的支持仅到此为止:
Mypy does not and cannot understand arbitrary metaclass code.
问题是,如果您将方法猴子修补到元class 的__new__
方法中的class,您可能会添加任何东西 到您 class 的定义。这对于 Mypy 来说太动态了,无法理解。
然而,并非一无所有!您在这里有几个选择。
选项 1:将方法静态定义为元上的实例方法class
类 是其元class 的实例,因此元class classmethod
s defined in a class. As such, you can rewrite minimal_example.py
as follows, and MyPy will be happy:
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"
metaclass 实例方法和您的平均 classmethod
之间的唯一大区别是 metaclass 实例方法不能从 class 使用元class:
>>> 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'
选项 2:向 MyPy 承诺一个方法存在,但实际上没有定义它
在很多情况下,您将使用元class,因为您希望比静态定义方法时更动态。例如,您可能希望即时动态生成方法定义并将它们添加到使用元class 的classes。在这些情况下,选项 1 根本行不通。
在这些情况下,另一种选择是“承诺”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"
这 passes MyPy 很好,实际上相当干净。但是,这种方法存在局限性,因为无法使用 shorthand typing.Callable
语法表达可调用对象的全部复杂性。
选项 3:欺骗 MyPy
第三种选择是欺骗 MyPy。有两种明显的方法可以做到这一点。
选项 3(a)。使用 typing.TYPE_CHECKING
常量
对于静态类型检查程序,typing.TYPE_CHECKING
常量始终为 True
,在运行时始终为 False
。因此,您可以使用此常量将 class 的不同定义提供给 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"
这个passes 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 将 none 变得更聪明。
(如您所见,无需在 .pyi
文件中复制完整的方法定义。MyPy 只需要这些方法的签名,因此约定只是简单地填充一个.pyi
文件中的函数,带有文字省略号 ...
。)
此解决方案比使用 TYPE_CHECKING
常量更简洁。但是,我 不会 使用 .pyi
文件。尽可能少地使用它们。如果你的 .py
文件中有一个 class 而你的存根文件中没有它的副本,MyPy 将完全不知道它的存在并引发各种误报错误。请记住:如果您有一个 .pyi
文件,MyPy 将 完全忽略 包含您的运行时实现的 .py
文件。
在 .pyi
文件中复制 class 定义不利于 DRY,并冒着更新 .py
文件中的运行时定义但忘记更新 [=26] 的风险=] 文件。如果可能,您应该将 真正需要 一个单独的 .pyi
存根的代码隔离到一个短文件中。然后,您应该在项目的其余部分照常注释类型,并在其余代码需要时照常从 very_dynamic_classes.py
导入必要的 classes。