Neal Neal - 3 months ago 9
Python Question

Why does constructing a new object potentially alter the field of *every* instance of that class?

Here's a simplified version of the code in question:

class thing:
def __init__(self, data1, data2={'foo': 1}):
self.data2 = data2
self.data2['bar'] = data1

datas = ['FIRST', 'SECOND']
things = [thing(x) for x in datas]
for p in things:
print p.data2['bar']


I would expect this code to return:

FIRST
SECOND


However, it actually returns:

SECOND
SECOND


Why?




My best guess is that I am creating a single dictionary
ur_dict = {'foo': 1}
, that when I initialize an object of class
thing
I am not creating a new dictionary
self.data2={'foo': 1}
but rather initializing a reference to
ur_dict
, and that when, in the constructor, I add the key-value pair
bar: data1
to
self.data2
, I'm actually adding that key and value to
ur_dict
itself. This would update the
data2
field of every single object in the list.

I tested this hypothesis by appending the following snippet to the code above:

things[0].data2['bar'] = 'FIRST'
for p in things:
print p.data2['bar']


Sure enough, the result is:

FIRST
FIRST


So I think my hypothesis is probably correct, but it's still mysterious to me. My question seems very related to this question --- when I create a default value in a constructor, is this value a class variable instead of an instance variable? Why doesn't python create new objects for default values of arguments in an object? Is there a good way to conceptualize or to catch this kind of error in the future? What are some good references to learn about what's going on here?

(Apologies in advance if I mangled jargon; I'm a mathematician by formal education.)




Edit:
@CrazyPython[::-1]
mentioned this is a property of functions, not classes or objects. I created an example of a function with a mutable default argument and I'm trying to figure out how to break it in the manner I experienced with objects and classes above. Came up with this:

EXPONENT = 1
def fooBar(x, y=EXPONENT):
return x**y
print EXPONENT
print foo(5)
EXPONENT = 2
print EXPONENT
print foo(5)


However, it returns:

1
5
2
5


What's a good way to "break" this to illustrate why not to use a mutable default argument here?

Answer

self.data2={'foo': 1} is a mutable default argument. Never do that. 99% of the time it's a mistake.

def foo(a, b={}):
    ...

is not equivalent to

def foo(a, b=None):
    if b is None:
        b = {}

b is not reconstructed every time. It's the same object.

Is there a good way to conceptualize or to catch this kind of error in the future?

Use PyCharm or another good editor

when I create a default value in a constructor, is this value a class variable instead of an instance variable?

no no no no no no. It's tied to the function, not the class or instance. This is behavior applies to all functions. It has nothing to do with classes or your linked question.

Why doesn't python create new objects for default values of arguments in an object?

Performance and compatibility. Constructing an object is a potentially expensive operation.


Addendum

  • Please use CamelCase for classes. Not doing so is a PEP 8 violation, which is the Python official style guide.
  • Get a decent editor. Personally, I recommend PyCharm. (I'm no salesperson) It's free.
    • PyCharm can rename variables, highlight PEP 8 errors, perform advanced auto complete, type checking, spot mutable default arguments, and much more.
  • You aren't inheriting off of object. That's bad.*

    Why does this python code alter the field of every object in a list?

  • Without context, that's a bad title. It asks about the code. When we read the title, we don't have the code.


Making it more clear

When a function is stated, the default arguments are evaluated. (not when it is executed, when it is stated) They are tied to the function. When a function is executed, a copy of the default arguments is not produced. You directly modify the default arguments. The next function that is called will receive the same object, with modifications.

*You seem to be a professor or the like. Please don't be offended by a meme. That's just the risk of getting advice from the internet.