在循环(或理解)中创建函数(或 lambda)

2024-11-18 08:40:00
admin
原创
12
摘要:问题描述:我正在尝试在循环内创建函数:functions = [] for i in range(3): def f(): return i functions.append(f) 或者,使用 lambda:functions = [] for i in range(3):...

问题描述:

我正在尝试在循环内创建函数:

functions = []

for i in range(3):
    def f():
        return i
    functions.append(f)

或者,使用 lambda:

functions = []

for i in range(3):
    functions.append(lambda: i)

问题是所有函数最终都一样。这三个函数不返回 0、1 和 2,而是返回 2:

print([f() for f in functions])
  • 预期输出:[0, 1, 2]

  • 实际产量: [2, 2, 2]

为什么会发生这种情况,我该怎么做才能获得分别输出 0、1 和 2 的 3 个不同函数?


解决方案 1:

您遇到了后期绑定的问题——每个函数都尽可能晚地查找i(因此,在循环结束后调用时,i将设置为2)。

通过强制早期绑定可以轻松修复:更改def f():def f(i=i):如下内容:

def f(i=i):
    return i

i默认值(中的右边i=i是参数名称的默认值,中的i左边是参数名称的默认值)是在时间时查找的,而不是在时间时查找的,因此本质上它们是一种专门寻找早期绑定的方法。i`i=idefcall`

如果您担心f获得额外的参数(因此可能被错误调用),那么有一种更复杂的方法,即使用闭包作为“函数工厂”:

def make_f(i):
    def f():
        return i
    return f

并在循环中使用f = make_f(i)而不是def语句。

解决方案 2:

解释

这里的问题是,创建i函数时不会保存的值。而是在调用时查找 的值。f`f`i

如果你仔细想想,就会发现这种行为完全合理。事实上,这是函数唯一合理的工作方式。想象一下,你有一个访问全局变量的函数,如下所示:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

当您阅读此代码时,您当然会期望它打印“bar”,而不是“foo”,因为global_var在声明函数后, 的值已更改。同样的事情也发生在您自己的代码中:当您调用 时f, 的值i已更改并被设置为2

解决方案

其实有很多方法可以解决这个问题。以下是一些选项:

  • i通过使用默认参数来强制早期绑定

与闭包变量(如i)不同,默认参数在函数定义时会立即求值:

for i in range(3):
    def f(i=i):  # <- right here is the important bit
        return i

    functions.append(f)

为了稍微了解一下它的工作原理/原因:函数的默认参数存储为函数的属性;因此,当前i被快照并保存。

>>> i = 0
>>> def f(i=i):
...     pass
>>> f.__defaults__  # this is where the current value of i is stored
(0,)
>>> # assigning a new value to i has no effect on the function's default arguments
>>> i = 5
>>> f.__defaults__
(0,)
  • 使用函数工厂捕获i闭包中的当前值

问题的根源在于,这是一个可以改变的变量。我们可以通过创建另一个i保证永远不会改变的变量来解决这个问题——最简单的方法是使用闭包

def f_factory(i):
    def f():
        return i  # i is now a *local* variable of f_factory and can't ever change
    return f

for i in range(3):           
    f = f_factory(i)
    functions.append(f)
  • 用于functools.partial将当前值绑定if

functools.partial可让您将参数附加到现有函数。从某种意义上说,它也是一种函数工厂。

import functools

def f(i):
    return i

for i in range(3):    
    f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
    functions.append(f_with_i)

警告:这些解决方案仅在为变量分配新值时才有效。如果您修改存储在变量中的对象,您将再次遇到相同的问题:

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

注意,i尽管我们将其转换为默认参数,但它仍然发生了变化!如果您的代码发生了变异 i,那么您必须将的副本i绑定到您的函数,如下所示:

  • def f(i=i.copy()):

  • f = f_factory(i.copy())

  • f_with_i = functools.partial(f, i.copy())

解决方案 3:

对于那些使用以下方式来回答这个问题的人lambda

解决方法很简单,就是用lambda: i替换lambda i=i: i

functions = []
for i in range(3): 
    functions.append(lambda i=i: i)
print([f() for f in functions])
# [0, 1, 2]

用例示例:如何让 lambda 函数立即评估变量(而不是推迟)

解决方案 4:

这不是答案,而是一条注释。

这种现象也可以在循环之外观察到,在那里可以很清楚地看到发生了什么:

g = 'hello'

def foo():
    return lambda x : (g,x)

f = foo()

print(f('world')); g = 'goodbye'; print(f('earth'))

我们(显然?)得到:

('hello', 'world')
('goodbye', 'earth')

或者可以查看问题中的代码,但通过调用并添加打印locals()输出globals()

functions = []

print(f"Before loop: local i = {locals().get('i')}, global i = {globals().get('i')}")

for i in range(3):
    print(f"Inside loop: local i = {locals().get('i')}, global i = {globals().get('i')}")
    def f():
        print(f"Inside f(): local i = {locals().get('i')}, global i = {globals().get('i')}")
        return i
    functions.append(f)

print(f"After loop: local i = {locals().get('i')}, global i = {globals().get('i')}")

print([f() for f in functions])

我们得到以下信息,这解释了很多:

Before loop: local i = None, global i = None
Inside loop: local i = 0, global i = 0
Inside loop: local i = 1, global i = 1
Inside loop: local i = 2, global i = 2
After loop: local i = 2, global i = 2
Inside f(): local i = None, global i = 2
Inside f(): local i = None, global i = 2
Inside f(): local i = None, global i = 2
[2, 2, 2]

归根结底,这是因为i问题中的循环不是局部的,而是程序的全局的(并且具有可变状态和闭包的语言有尴尬的角落)。此外,设计后 ALGOL60 ALGOL 风格的语言(其中变量不需要正确声明)的“有问题的”决定并没有帮助,即它使阅读 Python 程序变得非常困难(更不用说大型 Python 程序在经济上是危险的)。显然,PEP-3104在 2006 年提出了一个必要的改进想法,但它没有被采纳。

奖励回合:Java

由于我不确定 Java 会怎么做,所以这里出现了与 Java 中相同的问题。请注意,编译器需要付出额外的努力来检测闭包看到的变量是否是可变的(非最终的)并且在外部上下文中可见,还是只是本地的或最终的。整个现象并非偶然。

package org.example;

import java.util.*;

public class Main {


    private static String mutableOuterContextString;

    private static void print(String str) {
        System.out.println("  '" + str + "'");
    }

    private static void printStringWithIdentity(String str) {
        System.out.println("  " + stringWithIdentity(str));
    }

    private static String stringWithIdentity(String str) {
        return "'" + str + "' at " + Objects.toIdentityString(str);
    }

    private final static List<String> numberStrings = Collections.unmodifiableList(Arrays.asList("one", "two", "three"));

    // Here, closures will use the 'reference to String' as given
    // by 'str' at 'closure build time'. Each has been given a specific 'str'.
    // At 'closure call time', the closures created will properly print
    // "one", "two", "three".
    // This corresponds to "function returning function" approach in Python.

    public static List<Runnable> one_two_three_as_expected_1() {
        final List<Runnable> funcs = new ArrayList<>();
        numberStrings.forEach(str -> funcs.add(
                () -> print(str)
        ));
        return funcs;
    }

    // This is the same code as above, just more explicit.

    public static List<Runnable> one_two_three_as_expected_2() {
        final List<Runnable> funcs = new ArrayList<>();
        for (final String str : numberStrings) {
            funcs.add(
                    () -> print(str)
            );
        }
        return funcs;
    }

    // This is the same code as above, just even more explicit.
    // The closure is in fact "just a class" created by the compiler.

    private static class RunnableX implements Runnable {

        private final String str;

        public RunnableX(final String str) {
            this.str = str;
        }

        @Override
        public void run() {
            print(str);
        }
    }

    public static List<Runnable> one_two_three_as_expected_3() {
        final List<Runnable> funcs = new ArrayList<>();
        for (final String str : numberStrings) {
            funcs.add(new RunnableX(str));
        }
        return funcs;
    }

    // As in Python, an interaction between "mutable state in an
    // outside context" and closures leads to surprises.
    //
    // Syntactically, there is not much difference between the
    // closure closing over a local/final variable (str) or a
    // mutable variable from the outside context (mutableOuterContextString)
    // but the compiler must create some different code indeed.

    public static List<Runnable> threethreethree_by_accessing_outside_context_1() {
        final List<Runnable> funcs = new ArrayList<>();
        for (final String str : numberStrings) {
            mutableOuterContextString = str;
            funcs.add(
                    () -> printStringWithIdentity(mutableOuterContextString)
            );
        }
        return funcs;
    }


    // This should be the same code as above, just more explicit.
    // The closure is in fact "just a class" created by the compiler.

    private static class RunnableY implements Runnable {

        @Override
        public void run() {
            printStringWithIdentity(mutableOuterContextString);
        }
    }

    public static List<Runnable> threethreethree_by_accessing_outside_context_2() {
        final List<Runnable> funcs = new ArrayList<>();
        for (final String str : numberStrings) {
            mutableOuterContextString = str;
            funcs.add(new RunnableY());
        }
        return funcs;
    }

    // If the try to reproduce the "three three three" effect with a
    // variable in the local context, we get something that will not compile:
    // "Variable used in lambda expression should be final or effectively final"
    // at "System.out.println(curString2)"

    /*
    public static List<Runnable> three_three_three_this_will_not_compile() {
        final List<Runnable> funcs = new ArrayList<>();
        String curString2;
        for (final String str : numberStrings) {
            curString2 = str;
            funcs.add(() -> print(curString2)); // <--- won't compile
        }
        return funcs;
    }
    */

    // Fixing it Python-style
    // Note that we do not even need to declare a local variable inside the build_..() method.
    // Directly using the variable "outerStr" that has been passed-in is good enough.
    // It is not important whether it has been declared "final" or not in the method declaration.

    public static Runnable build_closure_with_its_own_local_variable(final String outerStr) {
        System.out.println("  Creating closure with a local reference for " + stringWithIdentity(outerStr));
        return () -> printStringWithIdentity(outerStr);
    }

    public static List<Runnable> three_three_three_fixed() {
        final List<Runnable> funcs = new ArrayList<>();
        for (final String str : numberStrings) {
            mutableOuterContextString = str;
            funcs.add(build_closure_with_its_own_local_variable(mutableOuterContextString));
        }
        return funcs;
    }

    public static void main(String[] args) {
        System.out.println("Print 'one', 'two', 'three' as expected, take 1");
        one_two_three_as_expected_1().forEach(r -> r.run());
        System.out.println("Print 'one', 'two', 'three' as expected, take 2");
        one_two_three_as_expected_2().forEach(r -> r.run());
        System.out.println("Print 'one', 'two', 'three' as expected, take 3");
        one_two_three_as_expected_3().forEach(r -> r.run());
        System.out.println("Print 'three', 'three', 'three', unexpectedly, take 1");
        threethreethree_by_accessing_outside_context_1().forEach(r -> r.run());
        System.out.println("Print 'three', 'three', 'three', unexpectedly, take 2");
        threethreethree_by_accessing_outside_context_2().forEach(r -> r.run());
        System.out.println("Print 'one', 'two', 'three' again by creating a local variable");
        three_three_three_fixed().forEach(r -> r.run());
    }
}

输出

Print 'one', 'two', 'three' as expected, take 1
  'one'
  'two'
  'three'
Print 'one', 'two', 'three' as expected, take 2
  'one'
  'two'
  'three'
Print 'one', 'two', 'three' as expected, take 3
  'one'
  'two'
  'three'
Print 'three', 'three', 'three', unexpectedly, take 1
  'three' at java.lang.String@77459877
  'three' at java.lang.String@77459877
  'three' at java.lang.String@77459877
Print 'three', 'three', 'three', unexpectedly, take 2
  'three' at java.lang.String@77459877
  'three' at java.lang.String@77459877
  'three' at java.lang.String@77459877
Print 'one', 'two', 'three' again by creating a local variable
  Creating closure with a local reference for 'one' at java.lang.String@87aac27
  Creating closure with a local reference for 'two' at java.lang.String@6ce253f1
  Creating closure with a local reference for 'three' at java.lang.String@77459877
  'one' at java.lang.String@87aac27
  'two' at java.lang.String@6ce253f1
  'three' at java.lang.String@77459877

解决方案 5:

问题在于i被绑定到循环变量,该变量在每次迭代时都会发生变化。这个问题有多种解决方案,可读性各不相同。在下面的 4 个代码片段中,底部的 3 个都是正确的,但我认为只有底部的那个最易读:

# Original: wrong
f_list = [
    (lambda x: x + i) for i in range(10)
]
print([f(3) for f in f_list])

# Correct, but not so intuitive
f_list = [
    (lambda x, i=i: x + i) for i in range(10)
]
print([f(3) for f in f_list])

# More intuitive, but not so readable
f_list = [
    (lambda i: (lambda x: x + i))(i) for i in range(10)
]
print([f(3) for f in f_list])

# More readable
get_f = lambda i: (lambda x: x + i)
f_list = [
    get_f(i) for i in range(10)
]
print([f(3) for f in f_list])

解决方案 6:

您可以尝试这样做:

l=[]
for t in range(10):
    def up(y):
        print(y)
    l.append(up)
l[5]('printing in 5th function')

解决方案 7:

您必须将每个值保存i在内存中的单独空间中,例如:

class StaticValue:
    val = None

    def __init__(self, value: int):
        StaticValue.val = value

    @staticmethod
    def get_lambda():
        return lambda x: x*StaticValue.val


class NotStaticValue:
    def __init__(self, value: int):
        self.val = value

    def get_lambda(self):
        return lambda x: x*self.val


if __name__ == '__main__':
    def foo():
        return [lambda x: x*i for i in range(4)]

    def bar():
        return [StaticValue(i).get_lambda() for i in range(4)]

    def foo_repaired():
        return [NotStaticValue(i).get_lambda() for i in range(4)]

    print([x(2) for x in foo()])
    print([x(2) for x in bar()])
    print([x(2) for x in foo_repaired()])

Result:
[6, 6, 6, 6]
[6, 6, 6, 6]
[0, 2, 4, 6]

解决方案 8:

除了@Aran-Fey 的优秀答案之外,在第二个解决方案中,您可能还希望修改函数内的变量,这可以通过关键字来完成nonlocal

def f_factory(i):
    def f(offset):
      nonlocal i
      i += offset
      return i
    return f

for i in range(3):           
    f = f_factory(i)
    print(f(10))

解决方案 9:

只需修改最后一行

functions.append(f())

编辑:这是因为f是一个函数 - python 将函数视为一等公民,您可以将它们传递到变量中以供稍后调用。因此,您的原始代码正在做的是将函数本身附加到列表中,而您想要做的是将函数的结果附加到列表中,这就是上面通过调用函数实现的。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用