Python 词汇闭包是如何工作的?
当我调查Javascript代码中的词法闭包问题时,我在Python中遇到了这个问题:Python 词汇闭包是如何工作的?,python,closures,lazy-evaluation,late-binding,python-closures,Python,Closures,Lazy Evaluation,Late Binding,Python Closures,当我调查Javascript代码中的词法闭包问题时,我在Python中遇到了这个问题: flist = [] for i in xrange(3): def func(x): return x * i flist.append(func) for f in flist: print f(2) 请注意,此示例有意避免使用lambda。它打印“4”,这是令人惊讶的。我希望是“024” 此等效的Perl代码正确地实现了这一点: my @flist = (); forea
flist = []
for i in xrange(3):
def func(x): return x * i
flist.append(func)
for f in flist:
print f(2)
请注意,此示例有意避免使用lambda
。它打印“4”,这是令人惊讶的。我希望是“024”
此等效的Perl代码正确地实现了这一点:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
“0 2 4”已打印
你能解释一下区别吗
更新: 问题不在于
i
是全局性的。这将显示相同的行为:
flist = []
def outer():
for i in xrange(3):
def inner(x): return x * i
flist.append(inner)
outer()
#~ print i # commented because it causes an error
for f in flist:
print f(2)
如注释行所示,
i
在该点是未知的。尽管如此,它仍会打印“4”。变量i
是一个全局变量,每次调用函数f
时其值为2
我倾向于实现您所追求的行为,如下所示:
>>> class f:
... def __init__(self, multiplier): self.multiplier = multiplier
... def __call__(self, multiplicand): return self.multiplier*multiplicand
...
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]
对您的更新的响应:导致这种行为的不是i
本身的全局性,而是它是一个封闭范围内的变量,在调用f时具有固定值。在您的第二个示例中,i
的值取自kkk
函数的作用域,当您在flist
上调用函数时,没有任何变化请参见以下内容:
for f in flist:
print f.func_closure
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
问题是所有的局部函数都绑定到同一个环境,从而绑定到同一个
i
变量。解决方案(变通方法)是为每个函数(或lambda)创建单独的环境(堆栈帧):
Python实际上是按照定义运行的。创建了三个独立的函数,但每个函数都有定义它们的环境的闭包——在本例中,是全局环境(如果循环放置在另一个函数中,则是外部函数的环境)。但这正是问题所在——在这种环境中,i发生了变异,所有闭包都引用相同的i 这里是我能想到的最好的解决方案——创建一个函数creater并调用它。这将为创建的每个函数强制使用不同的环境,每个函数中都有不同的i
flist = []
for i in xrange(3):
def funcC(j):
def func(x): return x * j
return func
flist.append(funcC(i))
for f in flist:
print f(2)
这就是当你混合了副作用和函数式编程时会发生的情况 循环中定义的函数在其值更改时不断访问同一变量
i
。在循环结束时,所有函数都指向同一个变量,该变量保存循环中的最后一个值:效果就是示例中报告的结果
为了计算i
并使用其值,一种常见模式是将其设置为参数默认值:在执行def
语句时计算参数默认值,从而冻结循环变量的值
以下工作如预期:
flist = []
for i in xrange(3):
def func(x, i=i): # the *value* of i is copied in func() environment
return x * i
flist.append(func)
for f in flist:
print f(2)
我仍然不完全相信为什么在某些语言中,这是一种方式,而在另一种方式。在公共Lisp中,它类似于Python:
(defvar *flist* '())
(dotimes (i 3 t)
(setf *flist*
(cons (lambda (x) (* x i)) *flist*)))
(dolist (f *flist*)
(format t "~a~%" (funcall f 2)))
打印“6”(请注意,这里的列表是从1到3,并反向构建)。
在Scheme中,它的工作方式与Perl中类似:
(define flist '())
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* i x)) flist)))
(map
(lambda (f)
(printf "~a~%" (f 2)))
flist)
打印“6 4 2”
正如我已经提到的,Javascript是Python/CL阵营中的一员。这里似乎有一个实现决策,不同的语言以不同的方式进行处理。我很想确切地了解决策是什么。发生的事情是捕获变量I,函数返回它绑定的值在函数式语言中,这种情况从未出现过,因为我不会反弹。但是在python中,正如您在lisp中看到的,这不再是事实 scheme示例的不同之处在于do循环的语义。scheme每次通过循环都会有效地创建一个新的i变量,而不是像其他语言一样重用现有的i绑定。如果使用在循环外部创建的不同变量并对其进行变异,您将在sche中看到相同的行为me.尝试用以下内容替换您的循环:
(let ((ii 1)) (
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* ii x)) flist))
(set! ii i))
))
让我们看看关于这方面的进一步讨论
[Edit]更好的描述方法可能是将do循环视为执行以下步骤的宏:
flist = []
def loop_body(i): # extract body of the for loop to function
def func(x): return x*i
flist.append(func)
map(loop_body, xrange(3)) # for i in xrange(3): body
i不再是来自父范围的变量,而是其自身范围内的一个全新变量(即lambda的参数),因此您可以观察到行为。Python没有这个隐式的新范围,因此for循环的主体只共享i变量。下面介绍如何使用
functools
库进行操作(我不确定在提出问题时是否有)
输出0 2 4,正如预期的那样。行为背后的原因已经解释过,并且发布了多种解决方案,但我认为这是最具Python风格的(记住,Python中的一切都是对象!):
Claudiu的答案很好,使用了函数生成器,但说实话,piro的答案是一个黑客,因为它使i变成了一个带有默认值的“隐藏”参数(它可以正常工作,但不是“pythonic”)。我不喜欢上面的解决方案如何在循环中创建
包装器
flist = []
def func(i):
return lambda x: x * i
for i in range(3):
flist.append(func(i))
for f in flist:
print f(2)
我的问题更“一般”。为什么Python会有这个缺陷?我希望有一种支持词法闭包的语言(比如Perl和整个Lisp王朝)为了正确地解决这个问题。询问某些东西为什么有缺陷是假设它不是缺陷。您的解决方案也是Javascript中使用的解决方案。这不是错误行为。它的行为完全符合定义。IMO piro有一个更好的解决方案,为了清楚起见,我可能会将最内层的“I”更改为“j”。这样定义它怎么样:def inner(x,I=I):返回x*i
s/在编译时/在执行def
语句时/这是一个i
flist = []
def loop_body(i): # extract body of the for loop to function
def func(x): return x*i
flist.append(func)
map(loop_body, xrange(3)) # for i in xrange(3): body
from functools import partial
flist = []
def func(i, x): return x * i
for i in xrange(3):
flist.append(partial(func, i))
for f in flist:
print f(2)
flist = []
for i in xrange(3):
def func(x): return x * func.i
func.i=i
flist.append(func)
for f in flist:
print f(2)
flist = []
def func(i):
return lambda x: x * i
for i in range(3):
flist.append(func(i))
for f in flist:
print f(2)