2

I have a problem with lambdas in a loop. It is hard to explain the problem so I will show it in a short example:

class TestObj():
    def __init__(self, value):
        self.value = value

lambdas = []
values_list = [10, 1]
for ele in values_list:
    foo = TestObj(ele)
    lambdas.append(lambda: foo.value > 5)

print([single_lambda() for single_lambda in lambdas])

as a result of print I get:

[False, False]

Despite I would expect [True, False]. Could you tell me why it does not work? and how Could I get the result that I expected? I only add that lambdas in a loop are required in my code to define formulas(those formulas are a lot and can be much more complex). Those formulas are defined in one place in the application and executed after many lines below the definition. I would be grateful for your help.

Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
Isel
  • 23
  • 3
  • First of all, please double check this: `self.value` in `__init__` should be `self.value = value` – Mikhail Beliansky Jun 17 '20 at 12:31
  • oh sorry, my bad, I made mistake during creating the post, in my test code is self.value = value. I will correct it here also. – Isel Jun 17 '20 at 12:34

3 Answers3

3

Lambdas are evaluated when they are executed.

They are executed when you print them. The only known foo at that time is the one from the last loop. So TestObj(1) is printed twice.

You can verify this by changing your lambda to:

lambdas.append(lambda: id(foo))   # will print the id() of the used foo

You need to "early" bind the foo from the loop to the lambda:

lambdas.append(lambda x = foo: x.value > 5) # bind x to foo from loop, check x.value

Full fix:

class TestObj():
    def __init__(self, value):
        self.value = value

lambdas = []
values_list = [10, 1]
for ele in values_list:
    foo = TestObj(ele)
    # use an x and bind it to this loops foo
    lambdas.append(lambda x = foo: x.value > 5)

# this is bad btw, use a simple for loop - do not use a list comp to create a list
# simply to print values 
print([single_lambda() for single_lambda in lambdas]) 

Output:

[True, False]

# and for the changed lambda with id() : the same id() twice

See Is it Pythonic to use list comprehensions for just side effects? regarding using list comprehension sideeffects and why its bad.


Related:

Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • 1
    Nice. Really useful post, thanks. I for one hadn't come across early binding with lambdas, hence the class workaround (see my other answer). I wonder why this is not more widely known and used. – alani Jun 17 '20 at 12:50
  • @ala this comes up 1-2 a month - mostly in combination with TK gui's - when binding functions to buttons - unfortunately can't find a good dupe and those posts are full of TK-related code. :) I am pretty sure someone with better google foo might be able to locate a dupe. – Patrick Artner Jun 17 '20 at 12:55
1

The problem is that the foo is merely lexically defined in the lambda, so it is not storing the value of foo from the time when the lambda was created.

Instead of a lambda, you could perhaps use a class to generate your callable. This can then store the associated state, namely the TestObj instance to which foo was pointing:

class TestObj():
    def __init__(self, value):
        self.value = value

class MyFunc():
    def __init__(self, testobj):
        self.testobj = testobj

    def __call__(self):
        return self.testobj.value > 5


funcs = []
values_list = [10, 1]
for ele in values_list:
    foo = TestObj(ele)
    funcs.append(MyFunc(foo))

print([single_func() for single_func in funcs])

output:

[True, False]
alani
  • 12,573
  • 2
  • 13
  • 23
0

For what you're doing you can use a map function. Small example:

a = [1, 2, 3]
res = map(lambda x: x < 2, a)
print(list(res))

# [True, False, False]

So for your example:

class TestObj:
    def __init__(self, value):
        self.value = value

values_list = [10, 1]
objects = [TestObj(ele) for ele in values_list]

res = map(lambda x: x.value > 5, objects)
print(list(res))

Or using list comprehension:

res = [obj.value > 5 for obj in objects]