简体   繁体   中英

What is the correct way of duplicating base class '__init__' signature in Python?

If a subclass does not define its own __init__ method, the constructor of the base class (hence its signature) is automatically inherited. But how one should define a sub-class __init__ method that inherits the signature of the base class (automatically)?

For example:

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

Child1 does not have its own constructor, hence Base 's __init__ is inherited, and help(Child1) would give:

Help on class Child1 in module test:

class Child1(Base)
 |  Child1(arg1, arg2)
 |  
 ...

Child2 needs to have a custom __init__ . However as it just passes all the arguments to Base , its arguments are defined as (*args, **kwargs) so as to short-circuit everything to Base . But this gives the following signature:

Help on class Child2 in module test:

class Child2(Base)
 |  Child2(*args, **kwargs)
 |  
 ...

which is much less informative. Is there a way to say "I want my __init__ to have the same signature as Base.__init__ "?

This answer is a description of how I figured out a reasonable solution, so please bear with me, or just scroll to the TL;DR'd decorator at the end.

Let's start by looking at what help is and does. help is added to the builtin namespace by site . The default implementation on my Ubuntu machine redirects everything to pydoc.help . This in turn uses inspect to get signatures and descriptions. You are only interested in functions, and more specifically __init__ . Also, you only care about the signature, not the rest of the docs. This should make things simpler.

We can safely assume that the signature you see in help / pydoc is generated by inspect.signature . Looking at the source code of that function for Python 3.8.2, and tracing through inspect.Signature.from_callable -> inspect._signature_from_callable -> Line 2246 , we see a possible solution.

The gist of it is is that if a function object has a __signature__ attribute, and that attribute is an instance of inspect.Signature , it will be used as the signature of the function, without recomputing it from the normal inspection of the __code__ and __annotation__ objects.

Another point in our favor is that functions are first-class objects with a__dict__ attribute that can have arbitrary keys assigned to it. Assigning __signature__ to your function will not affect its execution, since it is only used for inspection. The actual runtime signature is determined in the __code__ object through attributes like co_argcount , co_kwonlyargcount , co_varnames , etc.

You can therefore just do:

import inspect

Child2.__init__.__signature__ = inspect.signature(Base.__init__)

The result:

>>> 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)

Both constructors continue functioning as usual aside from that.

Since this does not modify the code object or even the annotations, the change is unlikely to affect anything with regards to the operation of the function.

TL;DR

Here is a decorator you can use to copy over function signatures without interfering with the function in any other way:

import inspect

def copy_signature(base):
    def decorator(func):
        func.__signature__ = inspect.signature(base)
        return func
    return decorator

And you could rewrite Child2 as

class Child2:

    @copy_signature(Base.__init__)
    def __init__(self, *args, **kwargs):
        ...

Sure, just specify it explicitly:

class Child2(Base):
    def __init__(self, arg1, arg2):
        if arg1 == 0:
            raise ValueError("arg1 cannot be zero")
        super().__init__(arg1, arg2)

using functools' wraps solved it for me

from functools import wraps
class Child2(Base):
    @wraps(Base.__init__, assigned=['__signature__']) # preserve base class signature for code completion
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM