Yep, a renowned Python footgun. Although Lambda container reuse adds a modern twist to it. Can I ask what editor you were using? Because decent ones should have warned you about mutable default args.
Python Anti-Pattern
11–20 of 27 posts
Re: Python Anti-Pattern
#12Linters can help a ton with stuff like this, I think pylint warns against this footgun. Also is there a reason for not using the shorter: var = var or [] Instead of: var = [] if var is None else var
Re: Python Anti-Pattern
#13As a Rubyist who only uses Python casually, it's neat to learn this difference! You can write very similar code in Ruby but such default objects appear to be created fresh on initialization avoiding the problem.
Re: Python Anti-Pattern
#14Yep, a renowned Python footgun. Although Lambda container reuse adds a modern twist to it. Can I ask what editor you were using? Because decent ones should have warned you about mutable default args.
Shout out to the amazing Pyright, the fast and feature-complete python LSP that you can use with any editor that supports the LSP protocol!
Re: Python Anti-Pattern
#15Linters can help a ton with stuff like this, I think pylint warns against this footgun. Also is there a reason for not using the shorter: var = var or [] Instead of: var = [] if var is None else var
def func(x=[]):
x.append(5)
return x
I've always been annoyed that pylint yells at you for this.Re: Python Anti-Pattern
#16Re: Python Anti-Pattern
#17Earlier quoted context omitted.
Shout out to the amazing Pyright, the fast and feature-complete python LSP that you can use with any editor that supports the LSP protocol!
I'm using VSCode with different Python specific extensions, like Pylance which uses Pyright, but it didn't warn me about the mutable function parameter. RIP
Re: Python Anti-Pattern
#18Linters can help a ton with stuff like this, I think pylint warns against this footgun. Also is there a reason for not using the shorter: var = var or [] Instead of: var = [] if var is None else var
I've never thought of this as a footgun. It's useful to accumulate state in a function, sort of like a static variable in c. E.g., def func(x=[]): x.append(5) return x I've always been annoyed that pylint yells at you for this.
Re: Python Anti-Pattern
#19About half the bugs I fix is due to unexpected interactions with mutable state. Now, I am usually the creator of the bugs, and it might be that I completely lacks any intellectual rigour with regards to mutable state, but it is re-assuring that I am bot the only one. This specific behaviour of python has always struck me as completely idiotic.
> This specific behaviour of python has always struck me as completely idiotic I'm curious, what would be your proposed solution? Make a special case exception for when the expression that constitutes a function default is evaluated? What would that exception look like?
class AntiAntiPattern:
2 """A quick and dirty attempt to mock a Python class instance that 'undoes' the antipattern
3 described here:
4 https://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html)"""
5 __mutable_args__ = {} # TODO: a real implementation would probably use a WeakMap of some sort
6 def __init__(self, a, b=None, c={}, d=set()):
7 self.a = a
8 self.b = "foo" if b is None else b
9 self.c = c
10 self.d = "bar" if d is None else d
11 def __getattr__(self, k):
12 return self.__mutable_args__[k] if k in self.__mutable_args__ else super().getattr(k)
13 def __setattr__(self, k, v):
14 if v in self.__init__.__defaults__:
15 self.__mutable_args__[k] = v
16 else:
17 self.__dict__[k] = v
18 def __repr__(self):
19 return json.dumps(dict(self.__dict__, **self.__mutable_args__), default=str)
>>> t = AntiAntiPattern(4, d = 'hello!')
>>> t.__mutable_args__
{'b': None, 'c': {}}
>>> t.__dict__
{'a': 4, 'd': 'hello!'}
>>> t
{"a": 4, "d": "hello!", "b": null, "c": {}}
Edit: Oops, fixed a tiny logic bug in __init__. Trying to ween myself off of the 'a = a or "my_a_default"' syntax as instructed by previous commenters in this thread. :)Edit2: It dawned on me that this doesn't actually resolve the 'gotcha' unless `v` is replaced with `deepcopy(v)` on line 15; otherwise the same exact problem rears its head:
>>> t = AntiAntiPattern(3, {})
>>> t.d
set()
>>> t.d.update([3,4])
>>> t
{"a": 3, "b": {}, "c": {}, "d": "{3, 4}"}
>>> t2 = AntiAntiPattern(4)
>>> t2
{"a": 4, "b": {}, "c": {}, "d": "{3, 4}"} # aw, heck
In which case I gather `__mutable_args__` is superfluous, and the solution involves simply making sure that class instances avoid pointing to the same memory objects?Re: Python Anti-Pattern
#20Earlier quoted context omitted.
> This specific behaviour of python has always struck me as completely idiotic I'm curious, what would be your proposed solution? Make a special case exception for when the expression that constitutes a function default is evaluated? What would that exception look like?
Possibly something like this? (Code for illustration purposes only, of course -- please no one use this.) class AntiAntiPattern: 2 """A quick and dirty attempt to mock a Python class instance that 'undoes' the antipattern 3 described here: 4 https://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html)""" 5 __mutable_args__ = {} # TODO: a real implementation would probably u…
By "what is your proposed solution?" I meant to ask "how would you change the language semantics to remove this 'idiotic' footgun", not "how would you work around this footgun?" I apologize for the ambiguity.
I initially thought this specific footgun was an unavoidable consequence in languages that primarily pass around references (like Python and Javascript). But it turns out that Javascript's default arguments do "make a special case exception for when the expression that constitutes a function default is evaluated!"
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...