Thomas Steinbach
Thomas Steinbach

Reputation: 1081

Python class fields as function parameters

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

Answers (1)

Wizard.Ritvik
Wizard.Ritvik

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

Related Questions