Automatically initialize instance variables?
Asked Answered
O

17

122

I have a python class that looks like this:

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):

followed by:

        self.PID=PID
        self.PPID=PPID
        self.cmd=cmd
        ...

Is there any way to autoinitialize these instance variables, like C++'s initialization list? It would spare lots of redundant code.

Oly answered 7/9, 2009 at 12:28 Comment(1)
See also discussion of the autoassign activestate recipe, and an alternate autoargs implementation at: What is the best way to do automatic attribute assignment in Python, and is it a good idea? - Stack OverflowCounterweigh
A
127

You can use a decorator:

from functools import wraps
import inspect

def initializer(func):
    """
    Automatically assigns the parameters.

    >>> class process:
    ...     @initializer
    ...     def __init__(self, cmd, reachable=False, user='root'):
    ...         pass
    >>> p = process('halt', True)
    >>> p.cmd, p.reachable, p.user
    ('halt', True, 'root')
    """
    names, varargs, keywords, defaults = inspect.getargspec(func)

    @wraps(func)
    def wrapper(self, *args, **kargs):
        for name, arg in list(zip(names[1:], args)) + list(kargs.items()):
            setattr(self, name, arg)

        for name, default in zip(reversed(names), reversed(defaults)):
            if not hasattr(self, name):
                setattr(self, name, default)

        func(self, *args, **kargs)

    return wrapper

Use it to decorate the __init__ method:

class process:
    @initializer
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        pass

Output:

>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
['FDs', 'PID', 'PPID', '__doc__', '__init__', '__module__', 'cmd', 'reachable', 'user'
Apophyllite answered 7/9, 2009 at 12:37 Comment(13)
minor nitpick - you forgot to import inspectIsolde
This works and answer the question so I voted up. But I kept Ferdidand Beyer answer: "Explicit is better than implicit"Dummy
+1 For great answer that solved my problem. But shouldn't it be a core functionality of the language? Do you think it's worth writing a PEP?Oly
@Nadia nice, but inside the wrapper fun should also be called, in case its body is more than just 'pass' -- no downsides to that! So pls edit this good answer to make it complete.Maracanda
@Udi, a PEP can't hurt, but you'll get lots of push-back (see e.g. Ferdinand Beyer's answer below). Then again, these days you ALWAYS get lots of push-back for ANY pep at all;-).Maracanda
This is a really good answer - this has gone straight into my toolbox.Johnajohnath
Thanks for this, it was pretty much exactly what I was looking for. Note that it won't work if you use keyword arguments or use default values, though you can pretty easily modify it to handle that case.Litalitany
@NadiaAlramli I think there is a small bug in the code. Look here gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)Unreel
The current example has a bug, and will not work if the signature doesn't include default arguments. You need to include a check to make sure names and defaults are not None. Ex: if names and defaults:Plaster
getargspec is going away, any port to python 3.5+?Rewrite
@NadiaAlramli Nice implementation : ), just added an answer based on your code that provides a few more use cases. getfullargspec returns a bit more info that is useful for some of the less common parameter structures.Hazelton
@Rewrite Check out my answer. It uses getfullargspec, which is the suggested replacement for getargspec via 3.6 documentation.Hazelton
In IDE like pycharm, such trick-way will make object.xxx of this class lossed its reference-origin(declaration)Pammy
L
67

For Python 3.7+ you can use a Data Class, which is a very pythonic and maintainable way to do what you want.

It allows you to define fields for your class, which are your automatically initialized instance variables.

It would look something like that:

@dataclass
class Process:
    PID: int
    PPID: int
    cmd: str
    ...

The __init__method will already be in your class.

Note that here type hinting is required, that is why I have used int and str in the example. If you don't know the type of your field, you can use Any from the typing module.

The Data Class has many advantages compared to the proposed solutions:

  • It is explicit: all fields are visible, which respects the Zen of Python and makes it readable and maintainable. Compare it to the use of **kwargs.
  • It can have methods. Just like any other class.
  • It allows you to go beyond the automatic __init__ using the __post_init__ method.

EDIT: Reasons to avoid using NamedTuples

Some suggest the use of namedtuple for this case, but namedtuples have some behaviours that differs from Python classes, which are not really evident at first and should be well known:

1. NamedTuples are immutable

Immutability can be useful, but maybe it is not what you want for your instances. DataClasses can also be somehow immutable if you use the argument frozen=True on the @dataclass decorator.

2. NamedTuples __eq__ behaves like Tuple's

In Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2) is True! The __eq__ function of NamedTuple, used in comparisons, only considers the values and the positions of those values on the compared instances, not their class or fields' names.

Loisloise answered 24/4, 2018 at 7:10 Comment(6)
This should only be used if the purpose of the class is to store data.Onieonion
Or to develop around storing data.Onieonion
Would you explain why dataclass should be used only for classes that store data, rather than have other behavior as well? I might create a new SO post for this entirely to better understand its appropriate use cases. Thanks.Cesium
Data Classes can be thought of as "mutable namedtuples with defaults". -- PEP557Anthropogeography
Using python 3.9.2 here. Decorator @dataclass not available for me. Why? (using debian instalation)Hensley
You need to import it. from dataclasses import dataclassLoisloise
D
37

If you're using Python 2.6 or higher, you can use collections.namedtuple:

>>> from collections import namedtuple
>>> Process = namedtuple('Process', 'PID PPID cmd')
>>> proc = Process(1, 2, 3)
>>> proc.PID
1
>>> proc.PPID
2

This is appropriate especially when your class is really just a big bag of values.

Darkroom answered 7/9, 2009 at 12:40 Comment(1)
"This is appropriate especially when your class is really just a big bag of values." In such a scenario, you could also do this: https://docs.python.org/3.3/tutorial/classes.html#odds-and-endsElissa
D
30

Quoting the Zen of Python,

Explicit is better than implicit.

Denigrate answered 7/9, 2009 at 12:38 Comment(6)
Wouldn't an initialization list declaration be explicit enough?Oly
I guess. But I don't see a reason for adding something like that to the language. I clearly prefer multiple assignment statements over some kind of decorator-magic behind the scenes.Denigrate
@Ferdinand, I agree it would be silly to have in the language something that can perfectly well be in the stdlib, but, it SHOULD be in the stdlib, because "beautiful is better than ugly" takes precedence, and many repetitive assignments are ugly (as is any form of repetition).Maracanda
Well, to counter: DWIM > DWISCreasy
I would agree decorator is more beautiful than a list of assignments but PyCharm makes it uglier by not understanding it :-(Marga
I agree. Decorating the function also makes complex worse than simple, since you cannot make inplace modifications to self.attributes inside init. A solution would need to be done at some other level before __init__ is called, probably using a metaclass.Onieonion
S
30

Another thing you can do:

class X(object):
    def __init__(self, a,b,c,d):
        vars = locals() # dict of local names
        self.__dict__.update(vars) # __dict__ holds and object's attributes
        del self.__dict__["self"] # don't need `self`

But the only solution I would recommend, besides just spelling it out, is "make a macro in your editor" ;-p

Sunderland answered 7/9, 2009 at 12:54 Comment(1)
Good catch on deleting 'self'.Catlin
B
15

You could do it easily with the keyword arguments, e.g. like this:

>>> class D:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

>>> D(test='d').test
'd'

similar implementation for the positional arguments would be:

>> class C:
    def __init__(self, *args):
        self.t, self.d = args


>>> C('abc', 'def').t
'abc'
>>> C('abc', 'def').d
'def'

which to me doesn't seem to solve your problem.

Bogtrotter answered 7/9, 2009 at 12:34 Comment(2)
Another variation that I like is self.__dict__.update( **kwargs )Immoderacy
Might as well use locals() and put normal arguments.Male
M
7

Nadia's solution is better and more powerful, but I think this is also interesting:

def constructor(*arg_names):
  def __init__(self, *args):
    for name, val in zip(arg_names, args):
      self.__setattr__(name, val)
  return __init__


class MyClass(object):
  __init__ = constructor("var1", "var2", "var3")


>>> c = MyClass("fish", "cheese", "beans")
>>> c.var2
"cheese"
Magog answered 3/12, 2009 at 3:8 Comment(0)
H
5

I needed something for the same purpose, but none of the existing answers covered all of the cases I tested.  Nadia's answer was the closest to what I was looking for, so I started with her code as a base.

The decorator below works with all valid combinations of arguments:

Positional                                          __init__(self, a, b                )
Keyword                                             __init__(self, a=None, b=None      )
Positional + Keyword                                __init__(self, a, b, c=None, d=None)
Variable Positional                                 __init__(self, *a                  )
Variable Positional + Keyword                       __init__(self, *a, b=None          )
Variable Positional + Variable Keyword              __init__(self, *a, **kwargs        )
Positional + Variable Positional + Keyword          __init__(self, a, *b, c=None       )
Positional + Variable Positional + Variable Keyword __init__(self, a, *b, **kwargs     )
Keyword Only                                        __init__(self, *, a=None           )
Positional + Keyword Only                           __init__(self, a, *, b=None        )

It also implements the standard _-prefix convention to allow for __init__-private variables that won't be assigned to class instances.


###  StdLib  ###
from functools import wraps
import inspect


###########################################################################################################################
#//////|   Decorator   |//////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def auto_assign_arguments(function):

  @wraps(function)
  def wrapped(self, *args, **kwargs):
    _assign_args(self, list(args), kwargs, function)
    function(self, *args, **kwargs)

  return wrapped


###########################################################################################################################
#//////|   Utils   |//////////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def _assign_args(instance, args, kwargs, function):

  def set_attribute(instance, parameter, default_arg):
    if not(parameter.startswith("_")):
      setattr(instance, parameter, default_arg)

  def assign_keyword_defaults(parameters, defaults):
    for parameter, default_arg in zip(reversed(parameters), reversed(defaults)):
      set_attribute(instance, parameter, default_arg)

  def assign_positional_args(parameters, args):
    for parameter, arg in zip(parameters, args.copy()):
      set_attribute(instance, parameter, arg)
      args.remove(arg)

  def assign_keyword_args(kwargs):
    for parameter, arg in kwargs.items():
      set_attribute(instance, parameter, arg)
  def assign_keyword_only_defaults(defaults):
    return assign_keyword_args(defaults)

  def assign_variable_args(parameter, args):
    set_attribute(instance, parameter, args)

  POSITIONAL_PARAMS, VARIABLE_PARAM, _, KEYWORD_DEFAULTS, _, KEYWORD_ONLY_DEFAULTS, _ = inspect.getfullargspec(function)
  POSITIONAL_PARAMS = POSITIONAL_PARAMS[1:] # remove 'self'

  if(KEYWORD_DEFAULTS     ): assign_keyword_defaults     (parameters=POSITIONAL_PARAMS,  defaults=KEYWORD_DEFAULTS)
  if(KEYWORD_ONLY_DEFAULTS): assign_keyword_only_defaults(defaults=KEYWORD_ONLY_DEFAULTS                          )
  if(args                 ): assign_positional_args      (parameters=POSITIONAL_PARAMS,  args=args                )
  if(kwargs               ): assign_keyword_args         (kwargs=kwargs                                           )
  if(VARIABLE_PARAM       ): assign_variable_args        (parameter=VARIABLE_PARAM,      args=args                )


###########################################################################################################################$#//////|   Tests   |//////////////////////////////////////////////////////////////////////////////////////////////////////#$###########################################################################################################################$$if __name__ == "__main__":$$#######|   Positional   |##################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2)$$#######|   Keyword   |#####################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a="KW_DEFAULT_1", b="KW_DEFAULT_2"):$      pass$$  t = T(a="kw_arg_1", b="kw_arg_2")$  assert (t.a == "kw_arg_1") and (t.b == "kw_arg_2")$$#######|   Positional + Keyword   |########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, c="KW_DEFAULT_1", d="KW_DEFAULT_2"):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, c="kw_arg_1")$  assert (t.a == 1) and (t.b == 2) and (t.c == "kw_arg_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, d="kw_arg_2")$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "kw_arg_2")$$#######|   Variable Positional   |#########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3])$$#######|   Variable Positional + Keyword   |###############################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, b="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3]) and (t.b == "KW_DEFAULT_1")$$  t = T(1, 2, 3, b="kw_arg_1")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1")$$#######|   Variable Positional + Variable Keyword   |######################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, **kwargs):$      pass$$  t = T(1, 2, 3, b="kw_arg_1", c="kw_arg_2")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1") and (t.c == "kw_arg_2")$$#######|   Positional + Variable Positional + Keyword   |##################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, c="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3, c="kw_arg_1")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1")$$#######|   Positional + Variable Positional + Variable Keyword   |#########################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, **kwargs):$      pass$$  t = T(1, 2, 3, c="kw_arg_1", d="kw_arg_2")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1") and (t.d == "kw_arg_2")$$#######|   Keyword Only   |################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *, a="KW_DEFAULT_1"):$      pass$$  t = T(a="kw_arg_1")$  assert (t.a == "kw_arg_1")$$#######|   Positional + Keyword Only   |###################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *, b="KW_DEFAULT_1"):$      pass$$  t = T(1)$  assert (t.a == 1) and (t.b == "KW_DEFAULT_1")$$  t = T(1, b="kw_arg_1")$  assert (t.a == 1) and (t.b == "kw_arg_1")$$#######|   Private __init__ Variables (underscored)   |####################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, _c):$      pass$$  t = T(1, 2, 3)$  assert hasattr(t, "a") and hasattr(t, "b") and not(hasattr(t, "_c"))

Note:

I included tests, but collapsed them into the last line (58) for brevity.  You can expand the tests, which detail all of the potential use cases, by find/replace-ing all $ characters with a newline.

Hazelton answered 12/5, 2017 at 21:27 Comment(0)
M
3

There may not be a need to initialize variables, as locals() already contains the values!

class Dummy(object):

def __init__(self, a, b, default='Fred'):
    self.params = {k:v for k,v in locals().items() if k != 'self'}

d = Dummy(2, 3)

d.params

{'a': 2, 'b': 3, 'default': 'Fred'}

d.params['b']

3

Of course, within a class one could use self.params

Myasthenia answered 20/1, 2014 at 19:31 Comment(1)
It's a nice and original approach, but d['b'] is written in Python's lingua franca while d.params['b'] will cause confusion for code readers.Oly
C
3

As soon as getargspec is deprecated since Python 3.5, here's solution using inspect.signature:

from inspect import signature, Parameter
import functools


def auto_assign(func):
    # Signature:
    sig = signature(func)
    for name, param in sig.parameters.items():
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            raise RuntimeError('Unable to auto assign if *args or **kwargs in signature.')
    # Wrapper:
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        for i, (name, param) in enumerate(sig.parameters.items()):
            # Skip 'self' param:
            if i == 0: continue
            # Search value in args, kwargs or defaults:
            if i - 1 < len(args):
                val = args[i - 1]
            elif name in kwargs:
                val = kwargs[name]
            else:
                val = param.default
            setattr(self, name, val)
        func(self, *args, **kwargs)
    return wrapper

Check if works:

class Foo(object):
    @auto_assign
    def __init__(self, a, b, c=None, d=None, e=3):
        pass

f = Foo(1, 2, d="a")
assert f.a == 1
assert f.b == 2
assert f.c is None
assert f.d == "a"
assert f.e == 3

print("Ok")
Chaise answered 21/2, 2016 at 21:21 Comment(0)
F
2

For Python 3.3+:

from functools import wraps
from inspect import Parameter, signature


def instance_variables(f):
    sig = signature(f)
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        values = sig.bind(self, *args, **kwargs)
        for k, p in sig.parameters.items():
            if k != 'self':
                if k in values.arguments:
                    val = values.arguments[k]
                    if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
                        setattr(self, k, val)
                    elif p.kind == Parameter.VAR_KEYWORD:
                        for k, v in values.arguments[k].items():
                            setattr(self, k, v) 
                else:
                    setattr(self, k, p.default) 
    return wrapper

class Point(object):
    @instance_variables 
    def __init__(self, x, y, z=1, *, m='meh', **kwargs):
        pass

Demo:

>>> p = Point('foo', 'bar', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

A non-decorator approach for both Python 2 and 3 using frames:

import inspect


def populate_self(self):
    frame = inspect.getouterframes(inspect.currentframe())[1][0]
    for k, v in frame.f_locals.items():
        if k != 'self':
            setattr(self, k, v)


class Point(object):
    def __init__(self, x, y):
        populate_self(self)

Demo:

>>> p = Point('foo', 'bar')
>>> p.x
'foo'
>>> p.y
'bar'
Flooring answered 25/7, 2017 at 8:30 Comment(0)
S
2

at the end of the init function:

for var in list(locals().keys()):
    setattr(self,var,locals()[var])

For more on setattr() please refer here

Sol answered 22/6, 2020 at 1:14 Comment(0)
K
1

nu11ptr has made a small module, PyInstanceVars, which includes this functionality as a function decorator. In the module's README is states that the "[...] performance is now only 30-40% worse than explicit initialization under CPython".

Usage example, lifted straight from the module's documentation:

>>> from instancevars import *
>>> class TestMe(object):
...     @instancevars(omit=['arg2_'])
...     def __init__(self, _arg1, arg2_, arg3='test'):
...             self.arg2 = arg2_ + 1
...
>>> testme = TestMe(1, 2)
>>> testme._arg1
1
>>> testme.arg2_
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'TestMe' object has no attribute 'arg2_'
>>> testme.arg2
3
>>> testme.arg3
'test'
Kibitzer answered 22/2, 2013 at 16:9 Comment(0)
A
1

There is a helper function to do this in the fastcore lib https://fastcore.fast.ai/utils.html#store_attr.

from fastcore.utils import store_attr

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        store_attr() # this will do the same as self.PID = PID etc.
Abott answered 28/9, 2020 at 15:26 Comment(0)
A
0

Maybe this is a closed question, but I would like to propose my solution in order to know what you think about it. I have used a metaclass which applies a decorator to init method

import inspect

class AutoInit(type):
    def __new__(meta, classname, supers, classdict):
        classdict['__init__'] = wrapper(classdict['__init__'])
        return type.__new__(meta, classname, supers, classdict)

def wrapper(old_init):
    def autoinit(*args):
        formals = inspect.getfullargspec(old_init).args
        for name, value in zip(formals[1:], args[1:]):
            setattr(args[0], name, value)
    return autoinit
Akanke answered 18/8, 2015 at 13:0 Comment(0)
B
0

The attrs library does something like this.

Bushhammer answered 11/6, 2017 at 18:37 Comment(0)
T
0

Simple solution here if you have fixed sets of inputs, you can use this:

from inspect import getargvalues, stack

def arguments():
    args = getargvalues(stack()[1][0])[-1]
    del args['self']
    if 'kwargs' in args:
        args.update(args['kwargs'])
        del args['kwargs']
    return args

class Process():
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        # Auto update all arguments into object dictionary
        self.__dict__.update(arguments())

This should do it, if you don't specify *kwargs

object = Process(1,2,3,'foo','random'...)
# all the right instances will be created
# object.PID =1
# object.PPID = 2

Timm answered 25/10, 2022 at 8:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.