This is pretty bizarre, and seems like it would cause a memory leak if you don't know what you're actually doing. You're allocating a my_dict object on the heap, in a field of the function object, so my_dict never goes out of scope and never gets garbage collected until the function fn itself does, right?
Whereas if you did this instead:
def fn(x):
my_dict={}
my_dict[x] = x * 2
return my_dict
You'd get what you'd expect--a new my_dict object gets created, returned, and then goes out of scope every time fn is called, so it would get garbage collected once there are no more references to that return value. (I think...)
(I don't know that much about how memory allocation and GC works in Python yet, just trying to learn!)
this is not at all bizarre, you just need to know what dynamic means. the best answer i could come up with is "definition is execution". for an enlightening moment, see this piece of code:
def a():
print "a called"
return []
def fn(x=a()):
x.append(1)
print x
fn()
fn()
fn()
i suggest typing this directly into the interpreter instead of a script for better effect.
You got me thinking about how python differs from lisp in this respect, and the subtle way that a functional API has helped this specific situation.
While lisp suffers from the same "definition is execution" gotcha, the effects are far rarer in practice because () is immutable and interned while [] is mutable and usually generated afresh each time it's executed.
$ python
>>> [] is []
False
$ sbcl
* (eq () ())
t
Since [] is mutable, appends of [] can do superficially 'the right thing'.
>>> a = []
>>> a.append(4)
>>> a
[4]
* (setq a ())
* (nconc a '(34))
(34)
* a
()
But there's a reason lisp does seemingly the wrong thing. According to the spec, nconc skips empty arguments (http://www.lispworks.com/documentation/HyperSpec/Body/f_ncon...) Reading between the lines, I'm assuming this makes sense from a perspective in lisp where we communicate even with 'destructive' operations through their return value. This is more apparent when you consider nreverse:
* (setq a '(1 2 3 4))
* (nreverse a)
(4 3 2 1)
* a
(1)
Destructive operations can reuse their input, but they're not required to maintain bindings. There is no precise equivalent of python's .reverse(). Instead, a common idiom is:
* (setq a (nreverse a))
It seems like a weird design decision, but one upshot of it besides encouraging a more functional style is that this optional-arg gotcha loses a lot of its power in lisps. It's very rare to define a default param of a non-empty list, and empty lists can't be modified without assigning to them.
Whereas if you did this instead:
You'd get what you'd expect--a new my_dict object gets created, returned, and then goes out of scope every time fn is called, so it would get garbage collected once there are no more references to that return value. (I think...)(I don't know that much about how memory allocation and GC works in Python yet, just trying to learn!)