Python function “remembering” earlier argument (**kwargs)

北慕城南 提交于 2019-12-30 10:04:29

问题


I have some objects that have a dictionary of attributes, obj.attrs. The constructor for these objects accepts a dict and/or **kwargs, for convenience.

It looks a little like this:

class Thing:
    def __init__(self, attrs={}, **kwargs):
        for arg in kwargs:
            attrs[arg] = kwargs[arg]
        self.attrs = attrs

Such that Thing({'color':'red'}) does the same as Thing(color='red').

My problem is that the constructor somehow remembers the last attrs value passed to it.

For example:

>>> thing1 = Thing(color='red')
>>> thing2 = Thing()
>>> thing2.attrs
{'color': 'red'}

...but thing2.attrs should just be an empty dict! {}

This made me wonder if this isn't a problem with using both **kwargs and an argument like attrs={}.

Any ideas?


回答1:


The problem with using default arguments is that only one instance of them actually exists. When you say attrs={} in your init method definition, that single default {} instance is the default for every call to that method (it doesn't make a new default empty dict every time, it uses the same one).

The problem is that if there's only one attrs in existence and then for every instance of Thing you say self.attrs = attrs, the self.attrs member variable for every single one of your instance is pointing to the single shared default instance of attrs.

The other question is, isn't this completely redundant? You can use **kwargs to pass in keyword/value args or a dictionary. If you just defined this:

class Thing:
    def __init__(self, **kwargs):
        for arg in kwargs:
            self.attrs[arg] = kwargs[arg]

All of these strategies still work:

thing1 = Thing(color='red')

thing2 = Thing(**{'color':'red'})

my_dict = {'color' : 'red'}
thing3 = Thing(**my_dict)

So if you simply define and use Thing that way, you'll avoid your problem altogether.




回答2:


What about changing the signature so that the dictionary is created each time

class Thing:
    def __init__(self, attrs=None, **kwargs):
        self.attrs = attrs or {}
        self.attrs.update(kwargs)



回答3:


attrs is a reference to a dictionary. When you create a new object, self.attrs points to that dictionary. When you assign a value from a kwargs, it goes to this dictionary.

Now, when you create a second instance, it's self.attrs also points to that same dictionary. Thus, it gets whatever data is in that dictionary.

For a nice discussion of this see "Least Astonishment" in Python: The Mutable Default Argument here on stackoverflow. Also see Default Parameter Values in Python on effbot.




回答4:


You want to change your code to:

class Thing:
    def __init__(self, attrs=None, **kwargs):
        attrs = {} if attrs is None else attrs
        for arg in kwargs:
            attrs[arg] = kwargs[arg]
        self.attrs = attrs

As others have pointed out, the value of a default argument is evaluated once, at definition time, not each time the function is called. By using a mutable container, each addition to the container is seen by all subsequent calls because each call uses the same container as the default value.

It might be that you only ever use attrs as a way to provide initial values, and you never meant to share dicts at all. In that case, use this:

class Thing:
    def __init__(self, attrs=None, **kwargs):
        self.attrs = {}
        if attrs:
            self.attrs.update(attrs)
        for arg in kwargs:
            self.attrs[arg] = kwargs[arg]



回答5:


Just for what it's worth - we can avoid the "attrs is a shared mutable object" problem by simply not mutating it. Instead of dumping the kwargs into attrs, dump them both into a new dict. Then the default-argument-object will always be {}.

class Thing:
    def __init__(self, attrs = {}, **kwargs):
        self.attrs = {}
        # Don't write the loop yourself!
        self.attrs.update(attrs)
        self.attrs.update(kwargs)

I'm only mentioning this because everyone's rushing to describe the "use None as a default argument and check for it" idiom, which I personally find rather hackish. sgusc has the right idea: the entire endeavour is not useful, given the general awesomeness of Python's **kwargs. :)



来源:https://stackoverflow.com/questions/6794285/python-function-remembering-earlier-argument-kwargs

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!