Python Anti-Pattern
valinsky.me
valinsky.me
Even if the process died, the container stopped, and the Lambda went “cold”, I wouldn’t even call that “dead”. It’s just cold. Dead would mean totally failed or decommissioned, I think.
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.
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?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...
def banana(parts = []):
pass
Calling that procedure without parts would mean parts would be bound to a NEW empty list, not the same old.Every new pyhton programmer is bitten by this sooner or later. Most people seem to think it is stupid, and relying on it seems, to me, to go against the python mantra of "being explicit", where explicit would mean either using an instance variable in the case of OO or a closure.
Regarding breaking backward compatibility in a future release, I'm not qualified to weigh in on the design decision of balancing between the disadvantages of breaking backward compatibility and the advantage of more intuitive semantics.
[0] https://github.com/microsoft/pyright/discussions/2306#discus...
Here's a stupid example. Notice in the first case printing i doesn't grab the value of i when the lambda is defined. The second case is likely what you want.
>>> lambdas = [lambda : print(i) for i in range(2)]
>>> for l in lambdas:
... l()
...
1
1
>>> lambdas = [lambda i=i: print(i) for i in range(2)]
>>> for l in lambdas:
... l()
...
0
1I haven't written python in many years, but in the following example it is at least clear what is going on:
def make_printer(n):
def printer():
print(n)
return printer
[make_printer(i) for i in range(2)]Can I ask what editor you were using? Because decent ones should have warned you about mutable default args.
I started a discussion in Pyright's repo here[0].
Also is there a reason for not using the shorter:
var = var or []
Instead of:
var = [] if var is None else var
It doesn't matter in this particular case because the only falsey List is `[]` anyway, but it's a bad habit.
JavaScript added the `??` operator for this specific problem.
from typing import List, Optional
def f(x: Optional[List[int]]): x = x or [] reveal_type(x)
mypy and pyright both show List[int] as type of x at the end and correctly drop None.
def func(x=[]):
x.append(5)
return x
I've always been annoyed that pylint yells at you for this.