如何在保留类型的同时重写类 __init__ 方法

问题描述 投票:0回答:3

在保持类型注释完整的同时扩展类

__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
可能是我无法完全控制的外部库。

python mypy python-typing
3个回答
3
投票

对于“向方法签名添加参数”的问题有一个解决方案 - 但它并不漂亮...... 使用 ParamSpecsConcatenate 您基本上可以捕获 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)。

请注意,这是打字模块的相当高级的用法。 如果您需要更多解释,可以在评论中告诉我。


0
投票

如果有人正在寻找一种方法让 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

类中拥有默认参数

-1
投票

真正的答案取决于您的用例。例如,您可以在类上实现

__new__()
特殊方法,以根据提供给
__init__()
的参数创建给定类型的对象。

在大多数情况下,这太复杂了,Python 为此提供了一种通用方法,涵盖了很多用例。看一下 Python 数据类

另一种方法可能是回顾我不久前写的对这个问题的更一般的回答。 “Python 中的 SubClassing Int”,特别是 Modified Type 类的示例。

© www.soinside.com 2019 - 2024. All rights reserved.