24

I'm running Python 3.4.2, and I'm confused at the behavior of my code. I'm trying to create a list of callable polynomial functions with increasing degree:

bases = [lambda x: x**i for i in range(3)]

But for some reason it does this:

print([b(5) for b in bases])
# [25, 25, 25]

Why is bases seemingly a list of the last lambda expression, in the list comprehension, repeated?

henrywallace
  • 633
  • 7
  • 16
  • 6
    You might find this helpful: [Gotcha: Python, scoping, and closures](http://eev.ee/blog/2011/04/24/gotcha-python-scoping-closures/) – unutbu Feb 01 '15 at 22:02
  • unutbu: Any idea on how to make it work? – L3viathan Feb 01 '15 at 22:04
  • Maybe what you want is `bases = lambda x:[x**i for i in range(3)]` ? – user3467349 Feb 01 '15 at 22:07
  • @user3467349 that's totally different. Then you'd do `base_results = bases(5)` – Adam Smith Feb 01 '15 at 22:18
  • @AdamSmith I see yeah.., then as far as I can see it this is an interpreter err... "retardation". This should `raise` as invalid (which could be reasonable) or work with proper iterator assignments... – user3467349 Feb 01 '15 at 22:23
  • @AdamSmith: Would you please describe how including the entire `lambda` in the list comprehension is different? Because my experiments show no difference in results (capabilities). What can be accomplished more easily thereby? – Tom Russell Apr 14 '18 at 19:49
  • @TomRussell `lambda: [...]` is a function that returns a list. `[lambda: ...]` is a list of functions. – Adam Smith Apr 14 '18 at 20:24
  • @TomRussell If this was something less trivial than a changing exponent, it could be very useful to have a list of functions. Consider building a list of buttons in a GUI, then a list of functions for each button to take, and assigning them all with `for (b, f) in zip(buttons, functions): b.configure(command=f)`. In this case it doesn't seem to be provide any benefit over `lambda xs: [x**i for (x, i) in enumerate(xs)]` or indeed `lambda x: [x**i for i in range(3)]` – Adam Smith Apr 14 '18 at 20:36
  • python for-comprehensions have represented about the largest disparity between its repute "this is great!" and reality. just another manifestation. When you're knee deep in "is this a generator" or "is it running the lambda/function" or "is the nested `nested list comprehension` supposed to be in this order or that one" .. and add things like *this* on top all of those headaches.. – WestCoastProjects Jan 06 '19 at 23:27
  • It's worth noting that this is a [FAQ in the official documentation](https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result). – John Y Sep 17 '19 at 20:45

4 Answers4

28

The problem, which is a classic "gotcha", is that the i referenced in the lambda functions is not looked up until the lambda function is called. At that time, the value of i is the last value it was bound to when the for-loop ended, i.e. 2.

If you bind i to a default value in the definition of the lambda functions, then each i becomes a local variable, and its default value is evaluated and bound to the function at the time the lambda is defined rather than called.

Thus, when the lambda is called, i is now looked up in the local scope, and its default value is used:

In [177]: bases = [lambda x, i=i: x**i for i in range(3)]

In [178]: print([b(5) for b in bases])
[1, 5, 25]

For reference:

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 2
    Neat (+1). It'd be awesome if the answer explained why the original code didn't work, and why this does. Thanks. – NPE Feb 01 '15 at 22:06
  • 1
    Is there any good reason for python interpreting the code like that? This is rather obfuscated... – user3467349 Feb 01 '15 at 22:25
  • @user3467349: Well, for now, Python uses [dynamic name resolution](http://en.wikipedia.org/wiki/Name_resolution#Static_versus_dynamic). But [the docs state](https://docs.python.org/3/tutorial/classes.html), "the language definition is evolving towards static name resolution", so maybe in the future the rules will change. – unutbu Feb 01 '15 at 22:52
  • @user3467349 The principle is called _closure_ and out there are people who find this behavior useful. – moooeeeep Feb 02 '15 at 08:38
  • But you could replicate the behaviour with `bases = [lambda x: x**($n-1) for i in range($n)]` (Where $n is a preset value) - so it's unclear why this this particular application *needs* to function as it does. (If there is a non-obfuscated way to do something it's almost always better to raise an error on the obfuscated way). – user3467349 Feb 02 '15 at 08:53
  • I wish I could see how it's somehow better to include the entire `lambda` expression in the list comprehension when it obfuscates the code and produces identical results AFAICT. – Tom Russell Apr 14 '18 at 19:46
  • while this answer is appreciated - as a clever workaround - I'd like to downvote `list comprehensions` yet another time. It is difficult to support the commonly held opinion that python is easy or powerful. – WestCoastProjects Jan 06 '19 at 23:29
4

As an alternate solution, you could use a partial function:

>>> bases = [(lambda i: lambda x: x**i)(i) for i in range(3)]
>>> print([b(5) for b in bases])
[1, 5, 25]

The only advantage of that construction over the classic solution given by @unutbu is that way, you cannot introduce sneaky bugs by calling your function with the wrong number of arguments:

>>> print([b(5, 8) for b in bases])
#             ^^^
#             oups
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
TypeError: <lambda>() takes 1 positional argument but 2 were given

As suggested by Adam Smith in a comment bellow, instead of using "nested lambda" you could use functools.partial with the same benefit:

>>> import functools
>>> bases = [functools.partial(lambda i,x: x**i,i) for i in range(3)]
>>> print([b(5) for b in bases])
[1, 5, 25]
>>> print([b(5, 8) for b in bases])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
TypeError: <lambda>() takes 2 positional arguments but 3 were given
Sylvain Leroux
  • 50,096
  • 7
  • 103
  • 125
  • 2
    If you're going to build partial functions, you should use `functools.partial`! – Adam Smith Feb 01 '15 at 22:14
  • @Adam Yes. You are right. I've edited my answer accordingly. The one thing that bother me when using `partial` is that the error message reports a "wrong" number of required positional arguments. But this is a rather minor gotcha for a syntax that is more clear to understand. – Sylvain Leroux Feb 01 '15 at 22:22
4

a more 'pythonic' approach:
using nested functions:

def polyGen(degree):
    def degPolynom(n):
        return n**degree
    return degPolynom

polynoms = [polyGen(i) for i in range(5)]
[pol(5) for pol in polynoms]

output:

>> [1, 5, 25, 125, 625]

Kamyar Ghasemlou
  • 859
  • 2
  • 9
  • 24
2

I don't think the "why this happens" aspect of the question has been answered yet.

The reason that names non-local names in a function are not considered constants is so that these non-local names will match the behaviour of global names. That is, changes to a global name after a function is created are observed when the function is called.

eg.

# global context
n = 1
def f():
    return n
n = 2
assert f() == 2

# non-local context
def f():
    n = 1
    def g():
        return n
    n = 2
    assert g() == 2
    return g
assert f()() == 2

You can see that in both the global and non-local contexts that if the value of a name is changed, then that change is reflected in future invocations of the function that references the name. If globals and non-locals were treated differently then that would be confusing. Thus, the behaviour is made consistent. If you need the current value of a name to made constant for a new function then the idiomatic way is to delegate the creation of the function to another function. The function is created in the creating-function's scope (where nothing changes), and thus the value of the name will not change.

eg.

def create_constant_getter(constant):
    def constant_getter():
        return constant
    return constant_getter

getters = [create_constant_getter(n) for n in range(5)]
constants = [f() for f in getters]
assert constants == [0, 1, 2, 3, 4]

Finally, as an addendum, functions can modify non-local names (if the name is marked as such) just as they can modify global names. eg.

def f():
    n = 0
    def increment():
        nonlocal n
        n += 1
        return n
    return increment
g = f()
assert g() + 1 == g()
Dunes
  • 37,291
  • 7
  • 81
  • 97