12

When I run the following script, both lambda's run os.startfile() on the same file -- junk.txt. I would expect each lambda to use the value "f" was set to when the lambda was created. Is there a way to get this to function as I expect?

import os


def main():
    files = [r'C:\_local\test.txt', r'C:\_local\junk.txt']
    funcs = []
    for f in files:
        funcs.append(lambda: os.startfile(f))
    print funcs
    funcs[0]()
    funcs[1]()


if __name__ == '__main__':
    main()
  • similar to [generating functions inside loop with lambda expression in python](http://stackoverflow.com/questions/1841268/generating-functions-inside-loop-with-lambda-expression-in-python) – Rodrigue Jun 08 '12 at 13:23

2 Answers2

24

One way is to do this:

def main():
    files = [r'C:\_local\test.txt', r'C:\_local\junk.txt']
    funcs = []
    for f in files:
        # create a new lambda and store the current `f` as default to `path`
        funcs.append(lambda path=f: os.stat(path))
    print funcs

    # calling the lambda without a parameter uses the default value
    funcs[0]() 
    funcs[1]()

Otherwise f is looked up when the function is called, so you get the current (after the loop) value.

Ways I like better:

def make_statfunc(f):
    return lambda: os.stat(f)

for f in files:
    # pass the current f to another function
    funcs.append(make_statfunc(f))

or even (in python 2.5+):

from functools import partial
for f in files:
    # create a partially applied function
    funcs.append(partial(os.stat, f))
Jochen Ritzel
  • 104,512
  • 31
  • 200
  • 194
  • 8
    This works because default arguments are bound at function definition time. –  May 17 '11 at 19:15
  • Ah, awesome. The generated lambdas are actually being used as event handlers for PyQt, so they can't have any input parameters, and I'm currently stuck in python 2.4, so I can't use functools, but the second solution -- passing it to another function -- works perfect. Thanks. –  May 17 '11 at 19:31
  • @Brendan, if you're define qt slots, you should be able to have input parameters with defaults, no? You also might also want to apply the decorator "PyQt4.QtCore.pyqtSlot" if you go with the second solution. – Neil G May 19 '11 at 18:28
5

It's important to understand that when a variable becomes part of a closure it's the variable itself and not the value being included.

This means that all the closures created in the loop are using the very same variable f, that at the end of the loop will contain the last value used inside the loop.

Because of how the language is defined however those captured variables are "readonly" in Python 2.x: any assignment makes a variable a local one unless it's declared global (Python 3.x adds the nonlocal keyword to allow writing to a local of the outer scope).

As Jochen Ritzel said in his answer the common idiom to avoid this variable capture and get instead value capture is to write

lambda f=f: os.startfile(f)

this works because default parameter values are evaluated at function creation time, and f is not the external variable but a function parameter that will have the value you want as default (so this lambda is just a function with default values for parameters, not closing any lexical variable any more).

6502
  • 112,025
  • 15
  • 165
  • 265