Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

bad recursion, still works

3 views
Skip to first unread message

iu2

unread,
Jul 15, 2008, 2:59:08 PM7/15/08
to
Hi,

I wrote this wrong recursive function that flattens a list:

def flatten(lst, acc=[]):
#print 'acc =', acc, 'lst =', lst
if type(lst) != list:
acc.append(lst)
else:
for item in lst:
flatten(item)
return acc

a = [1, 2, [3, 4, 5], [6, [7, 8, [9, 10], 11], 12], 13, 14]
b = flatten(a)
print b

I was amazed to realize that it flattens the list alright. Why? 'acc'
should be an empty list on each invocation of flatten, but is seems to
accumulate anyway...

mdsh...@gmail.com

unread,
Jul 15, 2008, 3:30:34 PM7/15/08
to

When you say acc=[] in the function declaration, it binds acc to a
particular list object, rather than to a concept of an empty list.
Thus, all operations being performed on acc are being performed on the
same list. If, after the sample code you provided, were to call

c = flatten([15,16,17,[18,19]])
print c

you would get back the list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19].

Mark Sherry

iu2

unread,
Jul 15, 2008, 4:12:28 PM7/15/08
to

I still don't understand: In each recursive call to flatten, acc
should be bound to a new [], shouldn't it? Why does the binding happen
only on the first call to flatten?

mdsh...@gmail.com

unread,
Jul 15, 2008, 4:39:33 PM7/15/08
to

Default values are bound when the function is defined, not when it's
called. For example,

>>> import random
>>> def foo(bar = random.random()):
... print bar
...
>>> foo()
0.632312549821312
>>> foo()
0.632312549821312

If you view [...] just as shorthand for list(...), it might make a bit
more sense. For immutable values, one can't change the value bound to
the name, only what the name is bound to, so this behaviour is less
obvious. But still there.

As to why default values are evaluated at define time vs. call time,
I'd argue reasons of scope and speed - if the default value is a
computed constant, it makes little sense to recompute it every time
the function is called.

Mark Sherry

Michael Torrie

unread,
Jul 15, 2008, 7:21:14 PM7/15/08
to iu2, pytho...@python.org
iu2 wrote:
> I still don't understand: In each recursive call to flatten, acc
> should be bound to a new [], shouldn't it? Why does the binding happen
> only on the first call to flatten?

Nope. In each new call it's (re)bound to the same original list, which
you've added to as your function continues--it's mutable. Default
variables that are bound to mutable objects are one of the big caveats
that is mentioned in the FAQ.

iu2

unread,
Jul 16, 2008, 3:47:35 AM7/16/08
to

Thanks guys, it's clear now

Jeff

unread,
Jul 16, 2008, 8:09:30 AM7/16/08
to

Is this avoidable by using a call to list() in the definition instead?

oj

unread,
Jul 16, 2008, 8:14:33 AM7/16/08
to

No.

Probably what you'd want to do, is something like this:

def func(arg1, arg2=None):
if arg2 is None:
arg2 = list()

...

So you create a list at runtime if arg2 has its default value.

Fredrik Lundh

unread,
Jul 16, 2008, 9:20:23 AM7/16/08
to pytho...@python.org
Jeff wrote:

> Is this avoidable by using a call to list() in the definition instead?

No. Default values are *always* evaluated when, and only when, the
"def" statement is executed; see:

http://docs.python.org/ref/function.html

Also note that "def" is an executable statement in Python, and that
default arguments are evaluated in the "def" statement's environment.
If you execute "def" multiple times, it'll create a new function object
(with freshly calculated default values) each time.

:::

The workaround is, as others have mentioned, to use a placeholder value
instead of modifying the default value. None is a common value:

def myfunc(value=None):
if value is None:
value = default()
# go on an modify value

If you need to handle arbitrary objects (including None), you can use a
sentinel object:

sentinel = object()

def myfunc(value=sentinel):
if value is sentinel:
value = default()

(in older code, written before "object" was introduced, you sometimes
see things like "sentinel = ['placeholder']" used to create a non-false
object with a unique identity; [] creates a new list every time it is
evaluated.)

:::

Finally, it should be noted that more advanced Python code often uses
this mechanism to its advantage; for example, if you create a bunch of
UI buttons in a loop, you might try something like:

for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)

only to find that all callbacks print the same value (most likely 9, in
this case). The reason for this is that Python's nested scopes bind to
variables, not object values, so all callback instances will see the
current (=last) value of the "i" variable. To fix this, use explicit
binding:

for i in range(10):
def callback(i=i):
print "clicked button", i
UI.Button("button %s" % i, callback)

The "i=i" part binds a local variable "i" to the *current* value of the
outer variable "i".

Two other uses are local caches/memoization; e.g.

def calculate(a, b, c, memo={}):
try:
value = memo[a, b, c] # return already calculated value
except KeyError:
value = do calculation on a, b, c
memo[a, b, c] = value # update the memo dictionary
return value

(this is especially nice for certain kinds of recursive algorithms)

and, for highly optimized code, local rebinding of global names:

def this_one_must_be_fast(x, sin=math.sin, cos=math.cos):
...

Hope this helps more than it confuses.

</F>

Peter Pearson

unread,
Jul 16, 2008, 2:24:04 PM7/16/08
to
On Wed, 16 Jul 2008 15:20:23 +0200, Fredrik Lundh wrote:
[snip]

> Hope this helps more than it confuses.

Absolutely. It is wonderfully enlightening. Many thanks.

--
To email me, substitute nowhere->spamcop, invalid->net.

Jeff

unread,
Jul 17, 2008, 8:27:29 AM7/17/08
to
Thanks, that made things very clear. I like that technique for adding
memoization via the parameter. That is clever. It would be nice if
there were a way to have multiple functions close over a shared local
variable in Python, like let-binding in lisp.

mdsh...@gmail.com

unread,
Jul 17, 2008, 1:30:48 PM7/17/08
to

Is this something like what you're looking for?

>>> def functionmaker():
... shared = []
... def addnumber():
... shared.append(3)
... return shared
... def addletter():
... shared.append('f')
... return shared
... return addnumber, addletter
...
>>> addnumber, addletter = functionmaker()
>>> addnumber()
[3]
>>> addletter()
[3, 'f']

0 new messages