5

I have come across this example from Python hitchhikers guide:

def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

The example above is the solution to some issues caused with late binding, where variables used in closures are looked up at the time the inner function is called.

What does the i=i mean and why is it making such difference?

Chen A.
  • 10,140
  • 3
  • 42
  • 61
py_script
  • 808
  • 2
  • 16
  • 38
  • 7
    It’s the syntax for optional parameters. Optional parameters’ defaults are evaluated when the functions themselves are evaluated, so making the default for the parameter `i` the current value of `i` solves the problem of the `i` taking on a different value later. – Ry- Nov 07 '17 at 19:16
  • https://stackoverflow.com/questions/7546285/creating-lambda-inside-a-loop – Josh Lee Nov 07 '17 at 19:32
  • Nominating as a duplicate of: https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture Not using dupe-hammer because I'm still on the fence... – juanpa.arrivillaga Nov 07 '17 at 19:35
  • Also related: https://stackoverflow.com/questions/12423614/local-variables-in-python-nested-functions – Ilja Everilä Nov 07 '17 at 19:38
  • 2
    Also, all of this is *explained in your link*... – juanpa.arrivillaga Nov 07 '17 at 19:39
  • 1
    By the way, `[(lambda i: (lambda x: i*x))(i) for i in range(5)]` is, arguably, are more conventional and less hacky alternative, which is equivalent to `list(map(lambda i: (lambda x: i*x), range(5)))` – Eli Korvigo Nov 07 '17 at 19:39
  • @EliKorvigo yes, that is certainly the "correct" way to do this, although, using the default-argument hack has become almost idiomatic in Python. – juanpa.arrivillaga Nov 07 '17 at 22:14

2 Answers2

3

In that case, each number in the range, will be assigned to the optional parameters of each lambda function:

def create_multipliers():
  return [lambda x, i=i : i * x for i in range(5)]

lambda x, i=0
lambda x, i=1
lambda x, i=2
lambda x, i=3
lambda x, i=4

So, you can call the functions now with one parameter (because they already have the default)

for f in create_multipliers():
  print(f(3))

0
3
6
9
12

Or you can call the function and give the parameter you want, that's why is optional

for f in create_multipliers():
  print(f(3,2))

6
6
6
6
6

There are examples where optional parameter are needed, such as recursion

For example, square in terms of square:

square = lambda n, m=0: 0 if n==m else n+square(n,m+1)

Look that the optional parameter there is used as accumulator

developer_hatch
  • 15,898
  • 3
  • 42
  • 75
3

It's actually not just for lambdas; any function that takes default parameters will use the same syntax. For example

def my_range(start, end, increment=1):
    ans = []
    while start < end:
        ans.append(start)
        start += increment
    return ans

(This is not actually how range works, I just thought it would be a simple example to understand). In this case, you can call my_range(5,10) and you will get [5,6,7,8,9]. But you can also call my_range(5,10,increment=2), which will give you [5, 7, 9].

You can get some surprising results with default arguments. As this excellent post describes, the argument is bound at function definition, not at function invocation as you might expect. That causes some strange behavior, but it actually helps us here. Consider the incorrect code provided in your link:

def create_multipliers():
    return [lambda x : i * x for i in range(5)]
for multiplier in create_multipliers():
    print multiplier(2)

When you call multiplier(2), what is it actually doing? It's taking your input parameter, 2, and returning i * 2. But what is i? The function doesn't have any variable called i in its own scope, so it checks the surrounding scope. In the surrounding scope, the value of i is just whatever value you left it -- in this case 4. So every function gives you 8.

On the other hand, if you provide a default parameter, the function has a variable called i in its own scope. What's the value of i? Well, you didn't provide one, so it uses its default value, which was bound when the function was defined. And when the function was defined, i had a different value for each of the functions in your list!

It is a bit confusing that they've used the same name for the parameter variable as they did for the iterating variable. I suspect you could get the same result with greater readability with

def create_multipliers():
    return [(lambda x, y=i: y*x) for i in range(5)]
colopop
  • 441
  • 3
  • 13