在循环(或理解)中创建函数(或 lambda)
- 2024-11-18 08:40:00
- admin 原创
- 17
问题描述:
我正在尝试在循环内创建函数:
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=idef
call`
如果您担心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
将当前值绑定i
到f
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 将函数视为一等公民,您可以将它们传递到变量中以供稍后调用。因此,您的原始代码正在做的是将函数本身附加到列表中,而您想要做的是将函数的结果附加到列表中,这就是上面通过调用函数实现的。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件