How to generate a list of different lambda functions with list comprehension?

前端 未结 4 444
有刺的猬
有刺的猬 2021-01-13 23:00

This question is distilled from the original application involving callback functions for Tkinter buttons. This is one line that illustrates the behavior.

lam

相关标签:
4条回答
  • 2021-01-13 23:11

    Use a parameter with default value to bind the current value of i to a local variable. When the lambda gets called without an argument, the local variable i is assigned the default value:

    In [110]: lambdas = [lambda i=i: i for i in range(3)]
    
    In [111]: for lam in lambdas:
       .....:       print(lam())
       .....: 
    0
    1
    2
    

    When i is not a local variable, Python looks up its value in the enclosing scope. The value that is found is the last value i attained in the for-loop of the list comprehension. That is why, without the parameter with default value, each lambda returns 2 since the for-loop has completed by the time the lambdas get called.


    Another common approach to solving this problem is to use a closure -- a function that can refer to environments that are no longer active such as the local namespace of an outer function, even after that function has returned.

    def make_func(i):
        return lambda: i
    
    lambdas = [make_func(i) for i in range(3)]
    for lam in lambdas:
        print(lam())
    

    prints

    0
    1
    2
    

    This works because when lam() gets called, since the i in the lambda function's body is not a local variable, Python looks for the value of i in the enclosing scope of the function make_func. Its local namespace is still accessible to the closure, lam, even though make_func has already completed. The value in that local namespace is the value which was passed to make_func, which is, happily, the desired value of i.


    As already mentioned by mkrieger1, another way to create a new function with some argument values already supplied is to use functools.partial:

    lambdas = [functools.partial(lambda x: x, i) for i in range(3)]
    
    0 讨论(0)
  • 2021-01-13 23:21

    You can use functools.partial to create specialized functions from a more general one by partial application, which reduces the parameter count by one:

    from functools import partial
    lambdas = [partial(lambda x: x, i) for i in range(3)]
    

    Here, lambda x: x is the general identity function taking one argument, and you create three specializations of it, taking no arguments, and returning fixed values.

    0 讨论(0)
  • 2021-01-13 23:23

    Ahhh, further Googling found a solution (admittedly one I would not have stumbled upon myself). The desired behavior can be invoked by use of a default argument:

    lambdas = [lambda i=i: i for i in range(3)]

    0 讨论(0)
  • 2021-01-13 23:34

    The important thing to be understood here is, the functions are created during the evaluation of the list comprehension, but the value of i will be evaluated only during the execution of the functions.

    So, in your case, you have created three functions and all of them refer i. When those functions are invoked at runtime, i will have the value 2, because that was the last value bound to i in the last iteration of the for loop.


    Instead, you need to preserve the current value of i in each of the functions created. So, the common practice is to include a default parameter, like this

    >>> lambdas = [lambda i=i: i for i in range(3)]
    >>> lambdas[0]()
    0
    >>> lambdas[1]()
    1
    >>> lambdas[2]()
    2
    

    Now, lambda i=i: i, creates a default parameter i which will have the current value of the loop variable i. So, when the functions are executed, the i referred is actually the parameter passed to the function, not the loop variable.

    To avoid confusion, you can choose to use a different name for the variable, like this

    >>> lambdas = [lambda t=i: t for i in range(3)]
    

    If you are looking other ways to avoid this, you can use map function to apply the numbers to another function which will generate new functions with the current value, like this

    >>> lambdas = map(lambda x: lambda: x, range(3))
    >>> lambdas[0]()
    0
    >>> lambdas[1]()
    1
    >>> lambdas[2]()
    2
    

    Here, lambda x: lambda: x creates an anonymous function which accepts a single parameter x, and returns another function which doesn't accept any parameters but returns the value x.

    Note: Don't use this map form in actual code. It might reduce the readability of the code.

    0 讨论(0)
提交回复
热议问题