Reputation: 1081
I have a Python class with an __init__
method accepting different parameters:
class A:
def __init__(self, foo: str, bar: int):
pass
For different reasons I also provide a dataclass which defines all the above __init__
parameters as fields:
@dataclass
class AOpts:
foo: str
bar: int
One good reason for this dataclass is that subclasses of A
can easily pass through all __init__
parameteres to the parent class like following:
class B(A):
def __init__(self, new_param: bool, a_opts: AOpts):
super().__init__(**a_opts.__dict__)
example = B(new_param: True, AOpts(foo="hi", bar=1))
So class B
doesn't have to repeat all __init__
arguments of class A
. And the editor/IDE provides all information about class A
parameters by the AOpts
dataclass.
Now to my "problem": I would like to derive the class A
's __init__
parameters from the AOps
dataclass fields. Because currently both are decoupled from each other. On changes I have to take care that the fields and parameters are equal.
Any idea how to achieve this?
Here what I'd like to achieve in not working pseudo code:
class A:
def __init__(self, <fields_from(AOpts)>):
pass
test = A(foo="hi", bar=1)
It would be great if also the IDE would understand which arguments are allowed in class A
's __init__
method.
Upvotes: 0
Views: 1569
Reputation: 11720
My personal opinion is that you are complicating things a bit more than necessary. I feel like if you wanted to, you could eliminate the AOpts
class entirely and merge the declaration into A
. By having both A
and B
as dataclasses, your IDE should be able to infer that since B
subclasses from A
, then all the fields of A
are also acceptable parameters when creating a B
instance.
For example:
from dataclasses import dataclass
@dataclass
class A:
foo: str
bar: int
@dataclass
class B(A):
new_param: bool
This way, the IDE can also offer field autocompletion/hinting when you create a B
instance as follows:
example = B(new_param=True, foo='test', bar=3)
If absolutely you need to retain an AOpts
dataclass separately, for whatever reason, you can have them loosely coupled but instead create a base dataclass BaseA
and have both A
and AOpts
derive from them, as shown below.
from dataclasses import dataclass
@dataclass
class BaseA:
foo: str
bar: int
class A(BaseA):
def print_hello(self):
print('hello')
AOpts = BaseA
class B(A):
def __init__(self, new_param: bool, a_opts: AOpts):
# if you need to do a shallow copy, use `a_opts.__dict__.copy()`
self.__dict__ = a_opts.__dict__
example = B(new_param=True, a_opts=AOpts(foo='test', bar=2))
print(example) # B(foo='test', bar=2)
If you can avoid super call in B
to A.__init__()
- which is something I wouldn't normally recommend doing, but looks like it might be a good fit for this use case - then again it's a little faster to do a direct assignment, maybe with a dict.copy()
- that's assuming you don't have a A.__init__()
or similar defined in code, of course.
from dataclasses import dataclass
from timeit import timeit
@dataclass
class BaseA:
foo: str
bar: int
def __post_init__(self):
# reverse the string
self.foo = self.foo[::-1]
# negate the number
self.bar *= -1
class A(BaseA):
# it's strange, but this is needed to get the expected result for
# `BCallsInitA` (I swear I understood python once upon a time)
def __post_init__(self):
pass
def print_hello(self):
print('hello')
AOpts = BaseA
class BCallsInitA(A):
def __init__(self, new_param: bool, a_opts: AOpts):
super().__init__(**a_opts.__dict__)
class B(A):
def __init__(self, new_param: bool, a_opts: AOpts):
self.__dict__ = a_opts.__dict__
example = B(new_param=True, a_opts=AOpts(foo='hello world!', bar=123))
print(example) # B(foo='!dlrow olleh', bar=-123)
example = BCallsInitA(new_param=True, a_opts=AOpts(foo='hello world!', bar=123))
print(example) # BCallsInitA(foo='!dlrow olleh', bar=-123)
print('BCallsInitA: ', timeit("BCallsInitA(new_param=True, a_opts=AOpts(foo='test', bar=2))", globals=globals()))
print('B: ', timeit("B(new_param=True, a_opts=AOpts(foo='test', bar=2))", globals=globals()))
Upvotes: 1