What is the best way to do automatic attribute assignment in Python, and is it a good idea?

Question:

Instead of writing code like this every time I define a class:

class Foo(object): 
     def __init__(self, a, b, c, d, e, f, g):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        self.f = f
        self.g = g

I could use this recipe for automatic attribute assignment.

class Foo(object):
     @autoassign
     def __init__(self, a, b, c, d, e, f, g):
        pass

Two questions:

  1. Are there drawbacks or pitfalls associated with this shortcut?
  2. Is there a better way to achieve similar convenience?
Asked By: FMc

||

Answers:

If you have a lot of variables, you could pass one single configuration dict or object.

Answered By: leoluk

Is there a better way to achieve similar convenience?

I don’t know if it is necessarily better, but you could do this:

class Foo(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


>>> foo = Foo(a = 1, b = 'bar', c = [1, 2])
>>> foo.a
1
>>> foo.b
'bar'
>>> foo.c
[1, 2]
>>> 

Courtesy Peter Norvig’s Python: Infrequently Answered Questions.

Answered By: Manoj Govindan

There are some things about the autoassign code that bug me (mostly stylistic, but one more serious problem):

  1. autoassign does not assign an
    ‘args’ attribute:

    class Foo(object):
        @autoassign
        def __init__(self,a,b,c=False,*args):
            pass
    a=Foo('IBM','/tmp',True, 100, 101)
    print(a.args)
    # AttributeError: 'Foo' object has no attribute 'args'
    
  2. autoassign acts like a decorator.
    But autoassign(*argnames) calls a
    function which returns a decorator.
    To achieve this magic, autoassign
    needs to test the type of its first
    argument. If given a choice, I
    prefer functions not test
    the type of its arguments.

  3. There seems to be a considerable
    amount of code devoted to setting up
    sieve, lambdas within lambdas,
    ifilters, and lots of conditions.

    if kwargs:
        exclude, f = set(kwargs['exclude']), None
        sieve = lambda l:itertools.ifilter(lambda nv: nv[0] not in exclude, l)
    elif len(names) == 1 and inspect.isfunction(names[0]):
        f = names[0]
        sieve = lambda l:l
    else:
        names, f = set(names), None
        sieve = lambda l: itertools.ifilter(lambda nv: nv[0] in names, l)
    

    I think there might be a simpler way. (See
    below).

  4. for _ in
    itertools.starmap(assigned.setdefault,
    defaults): pass
    . I don’t think
    map or starmap was meant to call
    functions, whose only purpose is their
    side effects. It could have been
    written more clearly with the mundane:

    for key,value in defaults.iteritems():
        assigned.setdefault(key,value)
    

Here is an alternative simpler implementation which has the same functionality as autoassign (e.g. can do includes and excludes), and which addresses the above points:

import inspect
import functools

def autoargs(*include, **kwargs):
    def _autoargs(func):
        attrs, varargs, varkw, defaults = inspect.getargspec(func)

        def sieve(attr):
            if kwargs and attr in kwargs['exclude']:
                return False
            if not include or attr in include:
                return True
            else:
                return False

        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            # handle default values
            if defaults:
                for attr, val in zip(reversed(attrs), reversed(defaults)):
                    if sieve(attr):
                        setattr(self, attr, val)
            # handle positional arguments
            positional_attrs = attrs[1:]
            for attr, val in zip(positional_attrs, args):
                if sieve(attr):
                    setattr(self, attr, val)
            # handle varargs
            if varargs:
                remaining_args = args[len(positional_attrs):]
                if sieve(varargs):
                    setattr(self, varargs, remaining_args)
            # handle varkw
            if kwargs:
                for attr, val in kwargs.items():
                    if sieve(attr):
                        setattr(self, attr, val)
            return func(self, *args, **kwargs)
        return wrapper
    return _autoargs

And here is the unit test I used to check its behavior:

import sys
import unittest
import utils_method as um

class Test(unittest.TestCase):
    def test_autoargs(self):
        class A(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False):
                pass
        a=A('rhubarb','pie',debug=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)

        class B(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False,*args):
                pass
        a=B('rhubarb','pie',True, 100, 101)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)
        self.assertTrue(a.args==(100,101))        

        class C(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False,*args,**kw):
                pass
        a=C('rhubarb','pie',True, 100, 101,verbose=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)
        self.assertTrue(a.verbose==True)        
        self.assertTrue(a.args==(100,101))        

    def test_autoargs_names(self):
        class C(object):
            @um.autoargs('bar','baz','verbose')
            def __init__(self,foo,bar,baz,verbose=False):
                pass
        a=C('rhubarb','pie',1)
        self.assertTrue(a.bar=='pie')
        self.assertTrue(a.baz==1)
        self.assertTrue(a.verbose==False)
        self.assertRaises(AttributeError,getattr,a,'foo')

    def test_autoargs_exclude(self):
        class C(object):
            @um.autoargs(exclude=('bar','baz','verbose'))
            def __init__(self,foo,bar,baz,verbose=False):
                pass
        a=C('rhubarb','pie',1)
        self.assertTrue(a.foo=='rhubarb')
        self.assertRaises(AttributeError,getattr,a,'bar')

    def test_defaults_none(self):
        class A(object):
            @um.autoargs()
            def __init__(self,foo,path,debug):
                pass
        a=A('rhubarb','pie',debug=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)


if __name__ == '__main__':
    unittest.main(argv = sys.argv + ['--verbose'])

PS. Using autoassign or autoargs is compatible with IPython code completion.

Answered By: unutbu

One drawback: many IDEs parse __init__.py to discover an object’s attributes. If you want automatic code completion in your IDE to be more functional, then you may be better off spelling it out the old-fashioned way.

Answered By: Max
class MyClass(object):
    def __init__(self, **kwargs):
        for key, value in kwargs.iteritems():
            setattr(self, key, value)

You just can’t use *args, but you can store in some instance list (like self.args, don’t know)

Answered By: Rafael Sierra

Similar to the above, though not the same… the following is very short, deals with args and kwargs:

def autoassign(lcls):
    for key in lcls.keys():
        if key!="self":
            lcls["self"].__dict__[key]=lcls[key]

Use like this:

class Test(object):
    def __init__(self, a, b, *args, **kwargs):
        autoassign(locals())
Answered By: onno

This a simple implementation by judy2k:

from inspect import signature

def auto_args(f):
    sig = signature(f)  # Get a signature object for the target:
    def replacement(self, *args, **kwargs):
        # Parse the provided arguments using the target's signature:
        bound_args = sig.bind(self, *args, **kwargs)
        # Save away the arguments on `self`:
        for k, v in bound_args.arguments.items():
            if k != 'self':
                setattr(self, k, v)
        # Call the actual constructor for anything else:
        f(self, *args, **kwargs)
    return replacement


class MyClass:
    @auto_args
    def __init__(self, a, b, c=None):
        pass

m = MyClass('A', 'B', 'C')
print(m.__dict__)
# {'a': 'A', 'b': 'B', 'c': 'C'}
Answered By: Apalala

In this package you can now find

Note that this has been validated for python 3.5+

Answered By: smarie

From Python 3.7+ you can use a Data Class, which achieves what you want and more.

It allows you to define fields for your class, which are attributes automatically assigned.

It would look something like that:

@dataclass
class Foo:
    a: str
    b: int
    c: str
    ...

The __init__ method will be automatically created in your class, and it will assign the arguments of instance creation to those attributes (and validate the arguments).

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.

Answered By: Jundiaius
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.