Search code examples
pythonpython-decoratorsreadability

How to properly set Python class attributes from init arguments


As a Python programmer, I frequently declare classes similar to

class Foo:
  def __init__(self, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9):
    self.attr1 = attr1
    self.attr2 = attr2
    ...
    self.attr9 = attr9

    # init your class

The problem is that I do that over and over, and it feels very inefficient. In C++ the way to do this would be

class Foo {
  public:
    Foo(int, int, int, int, int);
    
  private:
    int attr1, attr2, attr3, attr4, attr5;
    
};

Foo::Foo(int attr1, int attr2, int attr3, int attr4, int attr5) : 
    attr1(attr1), attr2(attr2), attr3(attr3), attr4(attr4), attr5(attr5) {
    // init your class
}

Which I feel like it's slightly more efficient. My question is: is there a standard way to avoid having to attribute the arguments to the class for every argument? I can think of ways to program it, for example

def attribute_setter(*arg_names):
  def wrapper(func):
    def setter_init(self, *args, **kwargs):
      for i, name in enumerate(arg_names):
        if i < len(args):
          setattr(self, name, args[i])
        elif name in kwargs:
          setattr(self, name, kwargs[name])
      func(self, *args, **kwargs)
    return setter_init
  return wrapper

class Foo:
  @attribute_setter('attr1', 'attr2', 'attr3', 'attr4', 'attr5', 'attr6', 'attr7', 'attr8', 'attr9')
  def __init__(self, *args, **kwargs):
    # init your class
    pass

Which yields

>>> foo = Foo(1, 2, 3, 4, 5, attr6=6, attr7=7, attr8=8, attr9=9)
>>> foo.attr1
1
>>> foo.attr9
9

With a lot more error handling, this decorator might become safe to use, but my concern is the readability of the code and making sure I'm not violating any good practice principle.


Solution

  • This is exactly what the dataclasses module is for. You use it like this:

    from dataclasses import dataclass
    
    
    @dataclass
    class Foo:
      attr1: str
      attr2: int
      attr3: bool
      # ...etc...
    

    That gives you a class with an implicit __init__ method that takes care of all the parameter setting for you. You also get things like a useful __repr__ and __str__ predefined:

    >>> myvar = Foo(attr1="somevalue", attr2=42, attr3=False)
    >>> myvar
    Foo(attr1='somevalue', attr2=42, attr3=False)
    >>> print(myvar)
    Foo(attr1='somevalue', attr2=42, attr3=False)