Welcome, guest | Sign In | My Account | Store | Cart

This recipe defines a decorator @autoassign that makes methods assign some or all of their arguments automatically to attributes of self.

Python, 105 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
from functools import wraps
from inspect import getargspec, isfunction
from itertools import izip, ifilter, starmap

def autoassign(*names, **kwargs):
    """
    autoassign(function) -> method
    autoassign(*argnames) -> decorator
    autoassign(exclude=argnames) -> decorator
    
    allow a method to assign (some of) its arguments as attributes of
    'self' automatically.  E.g.
    
    >>> class Foo(object):
    ...     @autoassign
    ...     def __init__(self, foo, bar): pass
    ... 
    >>> breakfast = Foo('spam', 'eggs')
    >>> breakfast.foo, breakfast.bar
    ('spam', 'eggs')
    
    To restrict autoassignment to 'bar' and 'baz', write:
    
        @autoassign('bar', 'baz')
        def method(self, foo, bar, baz): ...

    To prevent 'foo' and 'baz' from being autoassigned, use:

        @autoassign(exclude=('foo', 'baz'))
        def method(self, foo, bar, baz): ...
    """
    if kwargs:
        exclude, f = set(kwargs['exclude']), None
        sieve = lambda l:ifilter(lambda nv: nv[0] not in exclude, l)
    elif len(names) == 1 and isfunction(names[0]):
        f = names[0]
        sieve = lambda l:l
    else:
        names, f = set(names), None
        sieve = lambda l: ifilter(lambda nv: nv[0] in names, l)
    def decorator(f):
        fargnames, _, _, fdefaults = getargspec(f)
        # Remove self from fargnames and make sure fdefault is a tuple
        fargnames, fdefaults = fargnames[1:], fdefaults or ()
        defaults = list(sieve(izip(reversed(fargnames), reversed(fdefaults))))
        @wraps(f)
        def decorated(self, *args, **kwargs):
            assigned = dict(sieve(izip(fargnames, args)))
            assigned.update(sieve(kwargs.iteritems()))
            for _ in starmap(assigned.setdefault, defaults): pass
            self.__dict__.update(assigned)
            return f(self, *args, **kwargs)
        return decorated
    return f and decorator(f) or decorator

#---------- Examples of use ------------------

>>> class Test(object): 
...      @autoassign('foo', 'bar')
...      def __init__(self, foo, bar=3, baz=6):
...          "some clever stuff going on here"
...          print 'baz =', baz 
... 
>>> class Test2(object):
...     @autoassign
...     def __init__(self, foo, bar): pass
... 
>>> class Test3(object):
...     @autoassign(exclude=('foo', 'bar'))
...     def __init__(self, foo, bar, baz=5, **kwargs): pass
... 
>>> t = Test(1, 2, 5) 
baz = 5
>>> u = Test(foo=8)
baz = 6
>>> v = Test2(10, 11)
>>> w = Test3(100, 101, foobar=102)
>>> 
>>> print Test.__init__.__doc__
some clever stuff going on here
>>> 
>>> print t.foo
1
>>> print t.bar
2
>>> 
>>> print u.foo
8
>>> print u.bar
3
>>> 
>>> print v.foo, v.bar # 10 11
10 11
>>> print w.baz, w.foobar # 5 102
5 102
>>> for obj, attr in ('w', 'foo'), ('w', 'bar'), ('t', 'baz'):
...     try:
...         getattr(globals()[obj], attr)
...     except AttributeError:
...         print '%s.%s raises AttributeError' % (obj, attr)
... 
w.foo raises AttributeError
w.bar raises AttributeError
t.baz raises AttributeError
>>>

This recipe was first suggested in a thread on comp.lang.python:

http://groups.google.com/group/comp.lang.python/browse_thread/thread/32b421bbe6caaeed/7a1bd9ff3b5b3ca2

The original idea for an 'autoassign' decorator came form Diez B. Roggisch (see thread above), I modified it a bit and improved it after feedback from several people on c.l.p.

4 comments

Vladimir Pouzanov 16 years, 1 month ago  # | flag

Excellent. Excellent snippet and very useful too!

Vladimir Pouzanov 16 years, 1 month ago  # | flag

Excellent. Excellent snippet and very useful too!

Drew Perttula 16 years, 1 month ago  # | flag

3 more alternate versions. Here's another one: http://bigasterisk.com/darcs/?r=waterworks;a=headblob;f=/waterworks/Tools.py

def initialize(ob, args):
    """In __init__, call initialize(self, locals()) to load all passed
    arguments."""

and http://bigasterisk.com/post/makeattrs has another one:

def make_attributes_from_args(*argnames):
    """
    This function simulates the effect of running
      self.foo=foo
    for each of the given argument names ('foo' in the example just
    now). Now you can write:
        def __init__(self,foo,bar,baz):
            copy_to_attributes('foo','bar','baz')
            ...
    instead of:
        def __init__(self,foo,bar,baz):
            self.foo=foo
            self.bar=bar
            self.baz=baz
            ...
    """
    ...code...


    """
    ----------------------------------------------------------------
    More discussion:

    I feel that the non-standard function (this one) is justified even
    though it is less clear than the comb code above. The comb code
    version is as offensive to me as this is:
      mylist.append('a')
      mylist.append('b')
      mylist.append('c')

    That code, while clear, punishes both the reader and programmer.
    I hope that none of you would ever write the above, and that you'd
    use something like "mylist.extend(['a','b','c'])" instead. My
    make_attributes_from_args function is to be used in an analagous
    manner. (Yes, certain situations will warrant code that is
    repetitive in some way or another. But I'm talking about
    self.foo=foo stuff, not those situations.)

    Another way to look at the attribute-assignment problem: There's
    only one 'program step' going on in the block-- some locals are
    getting copied to instance variables. One thing deserves one line.

    The disadvantage of the function is that it makes ordinary code
    harder to read for programmers who haven't read these
    instructions.  My only weapon against that problem is the choice
    of the function's name.  I deliberately chose 'make' and 'args'
    instead of 'set' and 'locals' because I want the reader to
    immediately think of the new attributes that are (probably) being
    created out of the arguments to __init__. The fact that this
    function will operate on any locals and set existing attributes to
    new values is incidental.

    I would be interested to hear better names for the function.  If I
    get a better one, I will convert all my code immediately.

    For another look at the same problem, see:
      http://twistedmatrix.com/users/acapnotic/keeparg.py.html
    """
ninou 15 years ago  # | flag

good code i just change line 51: self.__dict__.update(assigned) with * for k,v in assigned.iteritems(): setattr(self, k, v) * because it does not work with tools like sqlalchemy

Created by Arnaud Delobelle on Tue, 11 Mar 2008 (PSF)
Python recipes (4591)
Arnaud Delobelle's recipes (3)

Required Modules

  • (none specified)

Other Information and Tasks