views:

74

answers:

2

So here is some code that simplifies what I've been working on:

vars = {
    'a':'alice',
    'b':'bob',
}
cnames = ['charlie', 'cindy']

commands = []

for c in cnames:
    kwargs = dict(vars)
    kwargs['c'] = c
    print kwargs
    commands.append(lambda:a_function(**kwargs))

print commands

def a_function(a=None, b=None, c=None):
    print a
    print b
    print c

for c in commands:
    print "run for "+ repr(c)
    c()

And here is its output:

{'a': 'alice', 'c': 'charlie', 'b': 'bob'}
{'a': 'alice', 'c': 'cindy', 'b': 'bob'}
[<function <lambda> at 0x1001e9a28>, <function <lambda> at 0x1001e9e60>]
run for <function <lambda> at 0x1001e9a28>
alice
bob
cindy
run for <function <lambda> at 0x1001e9e60>
alice
bob
cindy

I would expect to get charlie, then cindy, why is cindy being displayed twice?

+3  A: 

A function's body isn't ran until the function is called. When you do lambda: a_function(**kwargs), kwargs isn't looked up until you actually call the function. At that point it's assigned to the last one you made in the loop.

One solution that gets the result you want would be to do commands.append(lambda kwargs=kwargs: a_function(**kwargs))

Mike Graham
cool, this works now vars = { 'a':'alice', 'b':'bob', } cnames = ['charlie', 'cindy'] commands = [] for c in cnames: kwargs = dict(vars) kwargs['c'] = c print kwargs commands.append(lambda kwargs=kwargs:a_function(**kwargs)) print commands def a_function(a=None, b=None, c=None): print a print b print c for c in commands: print "run for "+ repr(c) c()
NorthIsUp
ugh, didn't get code hi lighting for some reason there.
NorthIsUp
Comment formatting is frustratingly limited.
Mike Graham
I actually ended up storing a dict like this: {'kw':kwargs, 'cmd':lambda x:func(**x)}while doing the lambda kw=kwargs:func(**kw) worked for the example I posted, it didn't in my more complicated program I'm working on.
NorthIsUp
+3  A: 

You're encountering a classic binding-time problem, and @Mike's solution is the classic one. A good alternative is to write a higher order function:

def makecall(kwargs):
  def callit():
    return a_function(**kwargs)
  return callit

and use commands.append(makecall(kwargs)) in your loop. Both solutions work on the same principle (by forcing early binding through passage of an argument -- a plain argument in my case, a default value for a named argument in @Mike's); the choice is just a matter of style or elegance (me, while I tolerate lambda in super-simple cases, as long as the subtlest complication intervenes I vastly prefer good old def;-).

Alex Martelli
Indeed, using something like this or using `functools.partial` is probably a nicer solution. `lambda` aside, a keyword argument is a sort of ugly place to store state.
Mike Graham