Bizarre behavior with mutable default arguments

Arnaud Delobelle arnodel at googlemail.com
Tue Jan 1 15:26:23 EST 2008


On Jan 1, 6:48 pm, "Gabriel Genellina" <gagsl-... at yahoo.com.ar> wrote:
> En Tue, 01 Jan 2008 15:45:00 -0200, bukzor <workithar... at gmail.com>  
> escribi�:
[...]
> >> I'm confused by what you mean by 'early binding'. Can you give a quick-
> >> n-dirty example?
> > Is an 'early bound' variable synonymous with a 'static' variable (in
> > C)?
>
> No. It means, in which moment the name gets its value assigned. Usually  
> Python does "late binding", that is, names are resolved at the time the  
> code is executed, not when it's compiled or defined.
> Consider this example:
>
> z = 1
> def foo(a)
>    print a+z
> foo(3) # prints 4
> z = 20
> foo(3) # prints 23
>
> The second time it prints 23, not 4, because the value for z is searched  
> when the code is executed, so the relevant value for z is 20.
> Note that if you later assign a non-numeric value to z, foo(3) will fail.
>
> If you want to achieve the effect of "early binding", that is, you want to  
> "freeze" z to be always what it was at the time the function was defined,  
> you can do that using a default argument:
>
> z = 1
> def foo(a, z=z)
>    print a+z
> z = None
> foo(3) # prints 4
>
> This way, foo(3) will always print 4, independently of the current value  
> of z. Moreover, you can `del z` and foo will continue to work.
>
> This is what I think Chris Mellon was refering to. This specific default  
> argument semantics allows one to achieve the effect of "early binding" in  
> a language which is mostly "late binding". If someone changes this, he has  
> to come with another way of faking early binding semantics at least as  
> simple as this, else we're solving an [inexistant for me] problem but  
> creating another one.
>
> --
> Gabriel Genellina

Let me say again that I believe the current behaviour to be the
correct one.  But I don't think this 'early binding' is critical for
this sort of example.  There are lots of ways to solve the problem of
having persistent state across function calls, for example:

* using classes
* using modules
* or simply nested functions:

def getfoo(z):
    def foo(a):
        print a + z
    return foo

>>> z = 1
>>> foo = getfoo(z)
>>> z = None
>>> foo(3)
4

And with nonlocal, we could even modify z inside foo and this change
would persist across calls.  This will be a much cleaner solution than
the current def bar(x, y, _hidden=[startvalue]).

Also, note that it's easy to implement default arguments in pure
python-without-default-arguments using a decorator:

def default(**defaults):
    defaults = defaults.items()
    def decorator(f):
        def decorated(*args, **kwargs):
            for name, val in defaults:
                kwargs.setdefault(name, val)
            return f(*args, **kwargs)
        return decorated
    return decorator

Here is your example:

>>> z=1
>>> @default(z=z)
... def foo(a, z):
...     print a + z
...
>>> z=None
>>> foo(3)
4

Another example, using mutables:

>>> @default(history=[])
... def bar(x, history):
...     history.append(x)
...     return list(history)
...
>>> map(bar, 'spam')
[['s'], ['s', 'p'], ['s', 'p', 'a'], ['s', 'p', 'a', 'm']]

--
Arnaud




More information about the Python-list mailing list