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

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.

Leave a Comment