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

11,627

Solution 1

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.

Solution 2

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.

Solution 3

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.

Solution 4

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.

Solution 5

In this package you can now find

Note that this has been validated for python 3.5+

Share:
11,627
FMc
Author by

FMc

s/old boss/new boss/g

Updated on June 25, 2022

Comments

  • FMc
    FMc about 2 years

    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?
  • pberkes
    pberkes almost 14 years
    This is a quick solution to the problem, but it has two drawbacks: 1) you have to add much more code if you want to make sure that some arguments are passed to the function; 2) the signature of the function is completely opaque, and it cannot be used to understand how to call the function; one will need to rely on the docstring instead
  • FMc
    FMc almost 14 years
    Thanks a lot. This is very helpful.
  • m01
    m01 over 10 years
    I also found your autoargs incredibly handy. One issue I noticed is that it seems to throw an error if defaults is None, because the call to reversed fails. Am I right in thinking that this should be fixable by adding an if defaults: in front of the line for attr,val in zip(reversed(attrs),reversed(defaults)):, or does that have unintended sideeffects? The unit test passes.
  • unutbu
    unutbu almost 7 years
    @m01: Sorry for the very belated response. Thanks for the correction!
  • luckydonald
    luckydonald over 4 years
    Backport for Python 3.6: The PEP 557 references a sample implementation (permalink as of 2019-10-28) on GitHub which has a pypi/pip module as well. Therefore it can be installed by pip install dataclasses.