在Python中复制基类'__init__'签名的正确方法是什么?

kMa*_*ter 6 python inheritance python-3.x

如果子类没有定义自己的__init__方法,则自动继承基类的构造函数(因此其签名)。但是应该如何定义一个__init__继承基类签名(自动)的子类方法呢?

例如:

class Base:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

class Child1(Base):
    def foo(self):
        return self.arg1 + self.arg2


class Child2(Base):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if self.arg1 == 0:
            raise ValueError("arg1 cannot be zero")

    def foo(self):
        return self.arg2 / self.arg1
Run Code Online (Sandbox Code Playgroud)

Child1没有自己的构造函数,因此Bases __init__被继承,并且help(Child1)会给出:

Help on class Child1 in module test:

class Child1(Base)
 |  Child1(arg1, arg2)
 |  
 ...
Run Code Online (Sandbox Code Playgroud)

Child2需要有一个习惯__init__。然而,由于它只是将所有参数传递给Base,因此它的参数被定义(*args, **kwargs)为将所有参数短路到Base。但这给出了以下签名:

Help on class Child2 in module test:

class Child2(Base)
 |  Child2(*args, **kwargs)
 |  
 ...
Run Code Online (Sandbox Code Playgroud)

其信息量要少得多。有没有办法说“我希望我的__init__签名与”相同Base.__init__

Mad*_*ist 7

这个答案描述了我如何找到一个合理的解决方案,所以请耐心等待,或者滚动到最后的 TL;DR'd 装饰器。

让我们首先看看是什么help和做了什么。help被添加到内置命名空间中site。我的 Ubuntu 机器上的默认实现将所有内容重定向到pydoc.help. 这反过来又用于inspect获取签名和描述。您只对函数感兴趣,更具体地说__init__。另外,您只关心签名,而不关心文档的其余部分。这应该会让事情变得更简单。

help我们可以放心地假设您在/中看到的签名pydoc是由 生成的inspect.signature。查看Python 3.8.2 的该函数的源代码,并跟踪inspect.Signature.from_callable-> inspect._signature_from_callable->第 2246 行,我们看到了一个可能的解决方案。

其要点是,如果函数对象具有属性__signature__,并且该属性是 的实例inspect.Signature,则它将用作函数的签名,而无需从 和__code__对象的正常检查中重新计算它__annotation__

我们赞成的另一点是函数是一流的对象,__dict__其属性可以分配任意键。分配__signature__给您的函数不会影响其执行,因为它仅用于检查。实际的运行时签名是__code__通过co_argcountco_kwonlyargcountco_varnames等属性在对象中确定的。

因此你可以这样做:

import inspect

Child2.__init__.__signature__ = inspect.signature(Base.__init__)
Run Code Online (Sandbox Code Playgroud)

结果:

>>> help(Child1)
Help on class Child1 in module __main__:

class Child1(Base)
 |  Child1(arg1, arg2)
 |  
 |  Method resolution order:
 |      Child1
 |      Base
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  foo(self)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Base:
 |  
 |  __init__(self, arg1, arg2)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Base:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

>>> help(Child2)
Help on class Child2 in module __main__:

class Child2(Base)
 |  Child2(arg1, arg2)
 |  
 |  Method resolution order:
 |      Child2
 |      Base
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, arg1, arg2)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  foo(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Base:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
Run Code Online (Sandbox Code Playgroud)

除此之外,两个构造函数都继续照常运行。

由于这不会修改代码对象甚至注释,因此更改不太可能影响函数操作。

长话短说

这是一个装饰器,您可以使用它来复制函数签名,而不会以任何其他方式干扰该函数:

import inspect

def copy_signature(base):
    def decorator(func):
        func.__signature__ = inspect.signature(base)
        return func
    return decorator
Run Code Online (Sandbox Code Playgroud)

你可以重写Child2

class Child2:

    @copy_signature(Base.__init__)
    def __init__(self, *args, **kwargs):
        ...
Run Code Online (Sandbox Code Playgroud)