Assign function arguments to `self`
Asked Answered
S

10

30

I've noticed that a common pattern I use is to assign SomeClass.__init__() arguments to self attributes of the same name. Example:

class SomeClass():
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

In fact it must be a common task for others as well as PyDev has a shortcut for this - if you place the cursor on the parameter list and click Ctrl+1 you're given the option to Assign parameters to attributes which will create that boilerplate code for you.

Is there a different, short and elegant way to perform this assignment?

Scratchy answered 30/12, 2011 at 18:47 Comment(10)
personal opinion: boilerplate code such as this is a common indication of a missing language construct...Scratchy
hmmm.... could it be done with a decorator?Scratchy
almost any alternative will prevent PyDev's Ctrl+Space to auto-complete these attributes... such a shame...Scratchy
Yes you can do it with a decorator, but doing it explicitly will be better for you on the long run. Also it will not mess up PyDevs autocompletion if you do it explicitlyByelection
Doing it manually builds character.Grandfather
python.org/dev/peps/pep-0020 (in particular, "explicit is better than implicit", "simple is better than complex", "readability counts").Jacks
Most of the constructor parameters my classes take shouldn't be copied into self under the same name without transformation. I make stuff private, validate values, normalize them, fill in mutable objects which would be default values if default arguments worked differently, etc.Boru
If Eclipse already does this for you, then why try to fix what isn't broken?Revisionism
@Droogans - there are those who believe that depending on the IDE instead of being able to code with ease with a text editor is associated with the dark side :)Scratchy
I agree with @senderle's answer (especially the bit about needing to refactor if you have many params). However there are rare cases when having many params is the correct approach (consider init of an sklearn estimator). In this case I suggest using an editor with vertical editing capabilities and auto complete. I use Intellij with vim bindings, no problem!Cant
S
3

Decorator magic!!

>>> class SomeClass():
        @ArgsToSelf
        def __init__(a, b=1, c=2, d=4, e=5):
            pass

>>> s=SomeClass(6,b=7,d=8)
>>> print s.a,s.b,s.c,s.d,s.e
6 7 2 8 5

while defining:

>>> import inspect
>>> def ArgsToSelf(f):
    def act(self, *args, **kwargs):
        arg_names,_,_,defaults = inspect.getargspec(f)
        defaults=list(defaults)
        for arg in args:
            setattr(self, arg_names.pop(0),arg)
        for arg_name,arg in kwargs.iteritems():
            setattr(self, arg_name,arg)
            defaults.pop(arg_names.index(arg_name))
            arg_names.remove(arg_name)
        for arg_name,arg in zip(arg_names,defaults):
            setattr(self, arg_name,arg)
        return f(*args, **kwargs)
    return act

Of course you could define this decorator once and use it throughout your project.
Also, This decorator works on any object function, not only __init__.

Scratchy answered 31/12, 2011 at 10:43 Comment(3)
python probably already has code somewhere that matches args,kwargs,defaults into locals() or vars(), so I could save on implementing it again in this decorator - I'm open for suggestions...Scratchy
btw, I believe that in most cases @Elated is right, however under certain circumstances this approach could be very convenient. A language construct could be even more elegant than this decorator, and also PyDev could support it thus not losing Ctrl+space functionality. Alas...Scratchy
This is exactly how it should be done! And I don't agree with @Elated completely. Yes If you have to many arguments, this is a hint for refactoring. But still it is tedious to type even a few, especially when they are quite long, and anyways it is all boilerplate code, which isn't really necessary. The only reason, why I will not use this decorator, is that it doesn't work well with the LSP of my editor. But I'm quite sure, that there will be a native language extension in the future. Other languages, like php, have it already.Intramundane
K
10

You could do this, which has the virtue of simplicity:

>>>  class C(object):
    def __init__(self, **kwargs):
        self.__dict__ = dict(kwargs)

This leaves it up to whatever code creates an instance of C to decide what the instance's attributes will be after construction, e.g.:

>>> c = C(a='a', b='b', c='c')
>>> c.a, c.b, c.c
('a', 'b', 'c')

If you want all C objects to have a, b, and c attributes, this approach won't be useful.

(BTW, this pattern comes from Guido his own bad self, as a general solution to the problem of defining enums in Python. Create a class like the above called Enum, and then you can write code like Colors = Enum(Red=0, Green=1, Blue=2), and henceforth use Colors.Red, Colors.Green, and Colors.Blue.)

It's a worthwhile exercise to figure out what kinds of problems you could have if you set self.__dict__ to kwargs instead of dict(kwargs).

Kilovoltampere answered 30/12, 2011 at 23:57 Comment(3)
Can you add a link to Guido's discussion of this pattern and enums?Grillo
This should be the top solution, specifically for its elegance and high probability of successful implementation. It functions correctly, is unlikely to behave unexpectedly, and takes only one---at most two---lines.Pentagon
This is a very nice solution. In case explicit naming is desired, something like this can be done: def __init__(self, a, b, c): --> self.__dict__ = dict(vars())Plea
E
9

I sympathize with your sense that boilerplate code is a bad thing. But in this case, I'm not sure there even could be a better alternative. Let's consider the possibilities.

If you're talking about just a few variables, then a series of self.x = x lines is easy to read. In fact, I think its explicitness makes that approach preferable from a readability standpoint. And while it might be a slight pain to type, that alone isn't quite enough to justify a new language construct that might obscure what's really going on. Certainly using vars(self).update() shenanigans would be more confusing than it's worth in this case.

On the other hand, if you're passing nine, ten, or more parameters to __init__, you probably need to refactor anyway. So this concern really only applies to cases that involve passing, say, 5-8 parameters. Now I can see how eight lines of self.x = x would be annoying both to type and to read; but I'm not sure that the 5-8 parameter case is common enough or troublesome enough to justify using a different method. So I think that, while the concern you're raising is a good one in principle, in practice, there are other limiting issues that make it irrelevant.

To make this point more concrete, let's consider a function that takes an object, a dict, and a list of names, and assigns values from the dict to names from the list. This ensures that you're still being explicit about which variables are being assigned to self. (I would never suggest a solution to this problem that didn't call for an explicit enumeration of the variables to be assigned; that would be a rare-earth bug magnet):

>>> def assign_attributes(obj, localdict, names):
...     for name in names:
...         setattr(obj, name, localdict[name])
...
>>> class SomeClass():
...     def __init__(self, a, b, c):
...         assign_attributes(self, vars(), ['a', 'b', 'c'])

Now, while not horribly unattractive, this is still harder to figure out than a straightforward series of self.x = x lines. And it's also longer and more trouble to type than one, two, and maybe even three or four lines, depending on circumstances. So you only get certain payoff starting with the five-parameter case. But that's also the exact moment that you begin to approach the limit on human short-term memory capacity (= 7 +/- 2 "chunks"). So in this case, your code is already a bit challenging to read, and this would only make it more challenging.

Elated answered 30/12, 2011 at 19:46 Comment(2)
Since we're talking about arbitrary numbers of attributes, in python3, using locals().keys() is much more efficient than listing then names of the attributes manually. ex: assignAttributes(self, vars(), locals().keys())Stanzel
@Skyler, I think you may have missed my main premise. I specifically think it's a bad idea to name the attributes of a class implicitly. The names of attributes should be specified explicitly. It ensures a minimal standard of self-documentation. Have you ever tried to read code that does something like this? Not fun!Elated
S
5

Mod for @pcperini's answer:

>>> class SomeClass():
        def __init__(self, a, b=1, c=2):
            for name,value in vars().items():
                if name != 'self':
                    setattr(self,name,value)

>>> s = SomeClass(7,8)
>>> print s.a,s.b,s.c
7 8 2
Scratchy answered 30/12, 2011 at 19:30 Comment(3)
@ThiefMaster - removed the list comprehension. (you could give people a chance to correct their mistakes before taking action, guess this is true here as it is in life)Scratchy
Votes can be changed for an indefinite period after the related answer/question has been edited ;) So there's nothing to say against downvoting+commenting and later removing the downvote and/or upvoting.Kurland
List comprehension version of the above: [ setattr(self, var[0], var[1]) for var in vars().items() if var[0] != "self" ]Upolu
K
3

Your specific case could also be handled with a namedtuple:

>>> from collections import namedtuple
>>> SomeClass = namedtuple("SomeClass", "a b c")
>>> sc = SomeClass(1, "x", 200)
>>> print sc
SomeClass(a=1, b='x', c=200)
>>> print sc.a, sc.b, sc.c
1 x 200
Keary answered 30/12, 2011 at 23:5 Comment(0)
S
3

Decorator magic!!

>>> class SomeClass():
        @ArgsToSelf
        def __init__(a, b=1, c=2, d=4, e=5):
            pass

>>> s=SomeClass(6,b=7,d=8)
>>> print s.a,s.b,s.c,s.d,s.e
6 7 2 8 5

while defining:

>>> import inspect
>>> def ArgsToSelf(f):
    def act(self, *args, **kwargs):
        arg_names,_,_,defaults = inspect.getargspec(f)
        defaults=list(defaults)
        for arg in args:
            setattr(self, arg_names.pop(0),arg)
        for arg_name,arg in kwargs.iteritems():
            setattr(self, arg_name,arg)
            defaults.pop(arg_names.index(arg_name))
            arg_names.remove(arg_name)
        for arg_name,arg in zip(arg_names,defaults):
            setattr(self, arg_name,arg)
        return f(*args, **kwargs)
    return act

Of course you could define this decorator once and use it throughout your project.
Also, This decorator works on any object function, not only __init__.

Scratchy answered 31/12, 2011 at 10:43 Comment(3)
python probably already has code somewhere that matches args,kwargs,defaults into locals() or vars(), so I could save on implementing it again in this decorator - I'm open for suggestions...Scratchy
btw, I believe that in most cases @Elated is right, however under certain circumstances this approach could be very convenient. A language construct could be even more elegant than this decorator, and also PyDev could support it thus not losing Ctrl+space functionality. Alas...Scratchy
This is exactly how it should be done! And I don't agree with @Elated completely. Yes If you have to many arguments, this is a hint for refactoring. But still it is tedious to type even a few, especially when they are quite long, and anyways it is all boilerplate code, which isn't really necessary. The only reason, why I will not use this decorator, is that it doesn't work well with the LSP of my editor. But I'm quite sure, that there will be a native language extension in the future. Other languages, like php, have it already.Intramundane
G
1

You can do it via setattr(), like:

[setattr(self, key, value) for key, value in kwargs.items()]

Is not very beautiful, but can save some space :)

So, you'll get:

  kwargs = { 'd':1, 'e': 2, 'z': 3, }

  class P():
     def __init__(self, **kwargs):
        [setattr(self, key, value) for key, value in kwargs.items()]

  x = P(**kwargs)
  dir(x)
  ['__doc__', '__init__', '__module__', 'd', 'e', 'z']
Gies answered 30/12, 2011 at 19:3 Comment(6)
True, but then I can't define the function's argument list, which in most cases is preferredScratchy
You could define the class' attribute list by using a new-style class with __slots__.Dina
Why use a list comprehension rather than a loop? for key, value in kwargs.items(): setattr(self, key, value) is slightly shorter and yet more straightforward. I'd use iteritems() rather than items() in Py<3.Jacks
And for brevity, for a in kwargs.items(): setattr(self, *a)Jacks
-1 for using a list comprehension as a shortcut for a for-loop, this is not a good style and should not be promoted to new learners. (Creates a throwaway list of Nones which are the return values from all the setattr calls, for no purpose.)Keary
-1 too. For me it's uglier than simply assigning and you also loose which are the accepted attributes (added an answer explaining how to use a Bunch class which I think it's better).Coy
C
1

For that simple use-case I must say I like putting things explicitly (using the Ctrl+1 from PyDev), but sometimes I also end up using a bunch implementation, but with a class where the accepted attributes are created from attributes pre-declared in the class, so that I know what's expected (and I like it more than a namedtuple as I find it more readable -- and it won't confuse static code analysis or code-completion).

I've put on a recipe for it at: http://code.activestate.com/recipes/577999-bunch-class-created-from-attributes-in-class/

The basic idea is that you declare your class as a subclass of Bunch and it'll create those attributes in the instance (either from default or from values passed in the constructor):

class Point(Bunch):
    x = 0
    y = 0

p0 = Point()
assert p0.x == 0
assert p0.y == 0

p1 = Point(x=10, y=20)
assert p1.x == 10
assert p1.y == 20

Also, Alex Martelli also provided a bunch implementation: http://code.activestate.com/recipes/52308-the-simple-but-handy-collector-of-a-bunch-of-named/ with the idea of updating the instance from the arguments, but that'll confuse static code-analysis (and IMO can make things harder to follow) so, I'd only use that approach for an instance that's created locally and thrown away inside that same scope without passing it anywhere else).

Coy answered 31/12, 2011 at 18:8 Comment(2)
Nice, although this solves specifically the __init__ case, whereas the decorator is more generic in this senseScratchy
True, but I think that pattern is only common in the constructor and setter methods (but as setter methods usually have only 1 argument, changing the assign line for the decorator line only makes the come more unreadable to me).Coy
S
1

I solved it for myself using locals() and __dict__:

>>> class Test:
...     def __init__(self, a, b, c):
...         l = locals()
...         for key in l:
...             self.__dict__[key] = l[key]
... 
>>> t = Test(1, 2, 3)
>>> t.a
1
>>> 
Setiform answered 19/5, 2016 at 9:42 Comment(0)
C
0

Disclaimer

Do not use this: I was simply trying to create the answer closest to OPs initial intentions. As pointed out in comments, this relies on entirely undefined behavior, and explicitly prohibited modifications of the symbol table.

It does work though, and has been tested under extremely basic circumstances.

Solution

class SomeClass():
    def __init__(self, a, b, c):
        vars(self).update(dict((k,v) for k,v in vars().iteritems() if (k != 'self')))

sc = SomeClass(1, 2, 3)
# sc.a == 1
# sc.b == 2
# sc.c == 3

Using the vars() built-in function, this snippet iterates through all of the variables available in the __init__ method (which should, at this point, just be self, a, b, and c) and set's self's variables equal to the same, obviously ignoring the argument-reference to self (because self.self seemed like a poor decision.)

Canaletto answered 30/12, 2011 at 19:15 Comment(2)
docs.python.org/library/functions.html#vars explicitly disallows modifying vars().Jacks
disclaimer updated. i'm hesitant to take it down though, just because i feel it's a pretty cool answer :)Canaletto
P
0

One of the problems with @user3638162's answer is that locals() contain the 'self' variable. Hence, you end up with an extra self.self. If one doesn't mind the extra self, that solution can simply be

class X:
    def __init__(self, a, b, c):
        self.__dict__.update(locals())

x = X(1, 2, 3)
print(x.a, x.__dict__)

The self can be removed after construction by del self.__dict__['self']

Alternatively, one can remove the self during construction using dictionary comprehensions introduced in Python3

class X:
    def __init__(self, a, b, c):
        self.__dict__.update(l for l in locals().items() if l[0] != 'self')

x = X(1, 2, 3)
print(x.a, x.__dict__)
Planchette answered 5/12, 2017 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.