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

Writing code like this can be very repetitive:

class c:        def __init__(self, memberVariableNumberOne, memberVariableNumberTwo):                self. memberVariableNumberOne = memberVariableNumberOne                self. memberVariableNumberTwo = memberVariableNumberTwo

The above can be changed to:

class c:        def __init__(self, memberVariableNumberOne, memberVariableNumberTwo):                AssignMemberVariablesFromParameters()

Python, 67 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
import inspect

def AssignMemberVariablesFromParameters(exclude=None, onlyInclude=None):
    """Assign member variables in the caller's object from the caller's parameters.

    The caller should be a method associated with an object. Keyword arguments
    are supported, but variable arguments are not since they don't have names.
    exclude is an optional iterable that specifies names to be explicitly
    excluded. If the optional iterable onlyInclude is specified,
    parameters / member variables not in onlyInclude will be ignored.

    >>> class c:
    ...     def __init__(self, a, b, c=3, **kwargs):
    ...        AssignMemberVariablesFromParameters()
    ...
    ...     def ignore_a_b(self, a, b, c, d):
    ...        AssignMemberVariablesFromParameters(exclude=['a', 'b'])
    ...
    ...     def ignore_c_d(alternateNameForSelf, a, b, c, d):
    ...        AssignMemberVariablesFromParameters(onlyInclude=['a', 'b'])

    >>> x = c(1, 2, d=4)
    >>> (x.a, x.b, x.c, x.d)
    (1, 2, 3, 4)

    >>> x.ignore_a_b(10, 20, 30, 40)
    >>> (x.a, x.b, x.c, x.d)
    (1, 2, 30, 40)

    >>> x.ignore_c_d(100, 200, 300, 400)
    >>> (x.a, x.b, x.c, x.d)
    (100, 200, 30, 40)

    >>> class c:
    ...     __slots__ = ['a', 'b', 'c']
    ...
    ...     def __init__(self, a, b):
    ...        AssignMemberVariablesFromParameters()

    >>> x = c(1, 2)
    >>> (x.a, x.b)
    (1, 2)
    """

    args, varargs, varkw, defaults = inspect.getargvalues(inspect.stack()[1][0])

    self = args[0]

    if exclude == None:
        exclude = []
    else:
        exclude = [arg for arg in exclude]

    if onlyInclude == None:
        onlyInclude = [arg for arg in args if arg != self]
        if varkw:
            onlyInclude += [arg for arg in defaults[varkw].keys() if arg != self]

    for arg in onlyInclude:
        if arg not in exclude:
            if arg in defaults:
                value = defaults[arg]
            elif varkw and arg in defaults[varkw]:
                value = defaults[varkw][arg]
            else:
                value = None
            exec 'defaults[self].%s = %s' % (arg, 'value')

This does have the potential disadvantage of introducing “magic” into code which always creates the possibility of subtle bugs. To minimize this possibility it is recommended that the use of this function be made quite obvious (e.g., by always making it the first line of methods in which it is used).

The advantage of this “magic” is that member variable names are typed once instead of three times, reducing the likelihood of misspellings and making it easier to rename member variables.

This implementation requires the inspect module which was added in Python 2.1.

7 comments

Drew Perttula 21 years, 5 months ago  # | flag

another solution to the same problem. By coincidence, I recently wrote a similar function. My solution aims to be simpler and more obvious (less magic) to the reader who encounters a usage. Here it is, with my docstrings:

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
            ...
    """

    callerlocals=sys._getframe(1).f_locals
    callerself=callerlocals['self']
    for a in argnames:
        setattr(callerself,a,callerlocals[a])


    """
    ----------------------------------------------------------------
    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
    """
Hamish Lawson 21 years, 5 months ago  # | flag

More Pythonic alternative to using exec. Instead of

exec 'defaults[self].%s = %s' % (arg, 'value')

a more Pythonic alternative might be

setattr(defaults[self], arg, value)
Sean Ross 20 years, 10 months ago  # | flag

Here's a trimmed version with a suggested name change. I think the name __update__ works here for two reasons:

  1. The leading and trailing double underscores let users know that there's something special happening, and

  2. what this method is doing is updating the dictionary of an instance with another dictionary made up of key/value pairs from __init__'s parameter list, i.e., something like this is going on:

    self.__dict__.update(initparamsdict)

So, since what we're doing is updating a dictionary, in a special way, and the method to do that is called 'update', it seems like '__update__' would be a reasonable name.

def __update__(include=[], exclude=[]):

import inspect

args, varargs, varkw, defaults = inspect.getargvalues(inspect.stack()[1][0])

self = defaults[args[0]]

if not include:

    include = args[1:] # skip 'self'

    if varkw:

        include.extend(defaults[varkw].keys())

        defaults.update(defaults[varkw])

for attrname in include:

    if attrname not in exclude:

        setattr(self, attrname, defaults[attrname])

some testing

class c:

def __init__(self, a, b, c=3, **kwargs):

    __update__()



def ignore_a_b(self, a, b, c, d):

    __update__(exclude=['a', 'b'])



def ignore_c_d(self, a, b, c, d):

    __update__(include=['a', 'b'])

x = c(1,2,d=4)

print "x.__dict__ = %s"%x.__dict__

x.ignore_a_b(10,20,30,40)

print "x.__dict__ = %s"%x.__dict__

x.ignore_c_d(100,200,300,400)

print "x.__dict__ = %s"%x.__dict__

outputs

x.__dict__ = {'a': 1, 'c': 3, 'b': 2, 'd': 4}

x.__dict__ = {'a': 1, 'c': 30, 'b': 2, 'd': 40}

x.__dict__ = {'a': 100, 'c': 30, 'b': 200, 'd': 40}

Sean Ross 20 years, 10 months ago  # | flag

Here's a somewhat more explicit version:

def params(include=[], exclude=[]):

"returns a dictionary of parameter names and values"

import inspect

args, varargs, varkw, defaults = inspect.getargvalues(inspect.stack()[1][0])

if not include:

    include = args[:]

    if varkw:

        include.extend(defaults[varkw].keys())

        defaults.update(defaults[varkw])

return dict([(attrname, defaults[attrname])

    for attrname in include if attrname not in exclude])

This is used as follows:

class c:

def __init__(self, a, b, c=3, **kwargs):

    self.__dict__.update(params(exclude=['self']))

This method is less concise, but it makes it easier to understand what you're trying to do - add attributes to self's __dict__ using the names and values supplied in __init__'s parameter list. Plus, you have the added benefit of a generally useful method "params()".

Jimmy Retzlaff (author) 19 years, 11 months ago  # | flag

I like it - I've switched to this approach in my own code.

Jimmy Retzlaff (author) 19 years, 11 months ago  # | flag

One thing to watch out for with this approach is that new style classes that make use of __slots__ don't have a __dict__ attribute.

Jimmy Retzlaff (author) 19 years, 11 months ago  # | flag

Some relections after long term use. After using this in quite a bit of code over the past 18 months I've noticed some pitfalls. I still like the idea of not having to type (or mis-type) instance variable names 3 times in the constructor, but these pitfalls should be weighed against that advantage to see if you like the balance.

First, when inspecting old code it is sometimes nice to search for self.x to determine where the instance variable x was initialized. After scratching my head for a while as to why I can only find self.x being used and not being initialized, I eventually remember that this recipe is in use. This isn’t a huge deal, but it can be distracting when you’re trying to find a bug.

Also, the inspect module is not terribly fast at this kind of thing. This isn’t a problem for instantiating objects a few times, but the time to run AssignMemberVariablesFromParameters can dominate the creation time of small objects. For example take this code:

class c:
    def __init__(self, a, b):
        AssignMemberVariablesFromParameters()

for i in xrange(100):
    x = c(i, i)
    a = x.a
    b = x.b

Profiling this on Python 2.3.3 on Windows XP indicates that over 95% of the total run time is spent inside inspect.stack. So if you have a coordinate class and you need to create thousands of coordinates then you should keep an eye on how this approach impacts your performance requirements.

Finally, source code analysis tools like PyChecker don’t do well with this approach. Because of the magic involved, it appears that the parameters are never used and that the instance variables are never initialized.

A more generally applicable version of techniques like this can be found in:

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/201195