在保持类型注释完整的同时扩展类
__init__
方法的正确方法是什么?
以这个示例课程为例:
class Base:
def __init__(self, *, a: str):
pass
我想对
Base
进行子类化,并向 b
方法添加一个新参数 __init__
:
from typing import Any
class Sub(Base):
def __init__(self, *args: Any, b: str, **kwargs: Any):
super().__init__(*args, **kwargs)
这种方法的问题是现在
Sub
基本上接受任何东西。例如,mypy 会很乐意接受以下内容:
Sub(a="", b="", invalid=1). # throws __init__() got an unexpected keyword argument 'invalid'
我也不想在
a
中重新定义Sub
,因为Base
可能是我无法完全控制的外部库。
对于“向方法签名添加参数”的问题有一个解决方案 - 但它并不漂亮...... 使用 ParamSpecs 和 Concatenate 您基本上可以捕获 Base init 的参数并扩展它们。
连接只能添加新的位置参数。其原因在介绍 ParamSpec 的 PEP 中有说明。简而言之,当添加关键字参数时,如果该关键字参数已被我们扩展的函数使用,我们就会遇到问题。
查看此代码。它非常先进,但这样你就可以保留基类 init 的类型注释而无需重写它们。
from typing import Callable, Type, TypeVar, overload
from typing_extensions import ParamSpec, Concatenate
P = ParamSpec("P")
TSelf = TypeVar("TSelf")
TReturn = TypeVar("TReturn")
T0 = TypeVar("T0")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
@overload
def add_args_to_signature(
to_signature: Callable[Concatenate[TSelf, P], TReturn],
new_arg_type: Type[T0]
) -> Callable[
[Callable[..., TReturn]],
Callable[Concatenate[TSelf, T0, P], TReturn]
]:
pass
@overload
def add_args_to_signature(
to_signature: Callable[Concatenate[TSelf, P], TReturn],
new_arg_type0: Type[T0],
new_arg_type1: Type[T1],
) -> Callable[
[Callable[..., TReturn]],
Callable[Concatenate[TSelf, T0, T1, P], TReturn]
]:
pass
@overload
def add_args_to_signature(
to_signature: Callable[Concatenate[TSelf, P], TReturn],
new_arg_type0: Type[T0],
new_arg_type1: Type[T1],
new_arg_type2: Type[T2]
) -> Callable[
[Callable[..., TReturn]],
Callable[Concatenate[TSelf, T0, T1, P], TReturn]
]:
pass
# repeat if you want to enable adding more parameters...
def add_args_to_signature(
*_, **__
):
return lambda f: f
class Base:
def __init__(self, some_arg: float, *, some_kwarg: int):
pass
class Sub(Base):
# Note: you'll lose the name of your new args in your code editor.
@add_args_to_signature(Base.__init__, str)
def __init__(self, you_can_only_add_positional_args: str, /, *args, **kwargs):
super().__init__(*args, **kwargs)
Sub("hello", 3.5, some_kwarg=5)
VS-Code 为 Sub 提供以下类型提示:
Sub(str, some_arg: float, *, some_kwarg: int)
我不知道 mypy 是否可以与 ParamSpec 和 Concatenate 一起使用...
由于 VS-Code 中的错误,参数的位置未正确匹配(所设置的参数被关闭了 1)。
请注意,这是打字模块的相当高级的用法。 如果您需要更多解释,可以在评论中告诉我。
如果有人正在寻找一种方法让 VSCode 显示
__init__
的签名,其中包括来自父类和子类的参数,这是我发现的:
我能够使用 dataclass 模块来实现这一点。
from dataclasses import dataclass
@dataclass
class Base:
"""Base model to be extended"""
arg1: bool
arg2: bool
def __post_init__(self) -> None:
# Called after __init__ method
pass
@dataclass
class SubClass(Base):
"""Child model extending Base"""
arg3: bool
arg4: bool
def __post_init__(self) -> None:
# Called after __init__ method
pass
现在,当我将鼠标悬停在
SubClass
上时,VSCode 会显示包含所有四个参数的方法签名。我不必使用*args, **kwargs
。
我对这个解决方案的抱怨是,如果我在
Base
中有非默认参数,我就不能在
SubClass
类中拥有默认参数
真正的答案取决于您的用例。例如,您可以在类上实现
__new__()
特殊方法,以根据提供给 __init__()
的参数创建给定类型的对象。
在大多数情况下,这太复杂了,Python 为此提供了一种通用方法,涵盖了很多用例。看一下 Python 数据类
另一种方法可能是回顾我不久前写的对这个问题的更一般的回答。 “Python 中的 SubClassing Int”,特别是 Modified Type 类的示例。