Reputation: 2459
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__
"?
Upvotes: 7
Views: 2172
Reputation: 984
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)
Upvotes: 0
Reputation: 114230
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):
...
Upvotes: 7
Reputation: 73450
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)
Upvotes: -1