4 ms·
Oh, that's a wtf for sure. More to be scared of and more paranoid I'm going to appear to junior devs while reviewing merge requests. If I'm not mistaken there's
by iknownothow 4y ago
Oh, that's a wtf for sure. More to be scared of and more paranoid I'm going to appear to junior devs while reviewing merge requests. If I'm not mistaken there's a chapter in Effective Python about this, where they recommend never using lists or dicts as Class attributes or default values for parameters and instead use factories or use None and then initialize them with default values when a new instance is created.
- roflyear 4y agoYeah it is a little tricky, but using them as class attributes can actually be handy in some circumstances. Note to readers unfamiliar with Python: class attributes is a special thing, different than instance attributes. Definitely one of the trickier things with Python for sure.
- praptak 4y agoThanks for bringing this up, I didn't make the connection to default args. I think this behavior is actually "default args" but reversed. In case of default args it is the mutability that causes the nasty surprise (ooops, all calls share the same object, which is mutable). In case of the "+=" it is the _immutability_ that causes the surprise - I would not expect "obj.a += ..." to create an instance attribute that shadows the class one. I think the practical conclusion here is "don't call += on objects that don't support it - because it may work and this is not what you want!)".
- iknownothow 4y agoMaybe we are looking at it differently. To me, the behavior of "obj.a += (2, )" is the obvious and expected one i.e. I expect a tuple "(1, 2)" to be created and stored in "obj.a" and the class attribute "A.a" is untouched and remains "(1,)". The behavior of the "obj.b += [2,]" for the list is the real gotcha here where it correctly alters the value of "obj.b" but wtf'ingly also alters the value of the class attribute "A.b" which completely alters the behavior of A. I'm curious if you see it the same way?
- praptak 4y agoMy intuitions come from C and C++, so "+=" should always modify a single object (in C lingo it would be "lvalue") in place. Storing the result somewhere else than the left side of "+=" is counterintuitive to me. Side note: the object can be immutable, but then the (variable or attribute) left side should be replaced with a new immutable object.
- iknownothow 4y agoGotcha, I see where we differ. I've exclusively programmed in Python and since int, str and tuple are immutable in Python, I've always assumed "+=" operators create copies and don't update the object.
- kazinator 4y ago> int is immutable But what does that have to do with assignment? >>> a = 3 >>> a += 4 >>> print(a) 7 3 and 7 are immutable, yet a takes on those objects. Obviously, this is inspired by the C += operator, and behaves accordingly, when it's not being weird.
- iknownothow 4y agoIf you go back to the original comment from the user praptak, you will notice in his example the assignments "obj.a += (2,)" and "obj.b += [2,]". These assignments work exactly as expected no matter which school of thought you come from. The difference is in how the Class attributes are affected i.e. the values of "A.a" and "A.b". # obj.a += (2,) has no impact on A.a since tuples # are immutable and copies are made. # This is unexpected from a C/C++ intuition. >>> print(A.a) (1,) # obj.b += [2,] has an indirect impact on A.b # since lists are mutable and updates are made in place. # This is unexpected for beginners in Python since it # deviates from how int, str and tuples behave as "+=" # can be used for int, str, tuples and lists. >>> print(A.b) [1, 2] Edit: formatting
- 4y ago
- anthony_romeo 4y ago> If I'm not mistaken there's a chapter in Effective Python about this, where they recommend never using lists or dicts as Class attributes or default values for parameters and instead use factories or use None and then initialize them with default values when a new instance is created. Ya, this is because the initialized object is stored as the default parameter, and is not initialized every time the function runs. def functy(dicty={}): print('dicty=', dicty) return dicty output = functy() # dicty = {} output.update({'key': 'value'}) functy() # dicty = {'key': 'value'}