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.