Python 闭包中有什么?习惯 OCaml 的人有什么注意事项?
What is in a Python closure and what are the caveats for people used to OCaml?
这是对 an old answer to a question about the necessity of functools.partial 的一种跟进:虽然这个答案非常清楚地解释了这种现象及其基本原因,但我仍然有一些不清楚的地方。
回顾一下,以下 Python 代码
myfuns = [lambda arg: str(arg) + str(clo) for clo in range(4)]
try :
clo
except NameError :
print("there is no clo")
for arg in range(4) :
print(myfuns[arg](arg), end=", ")
给出03, 13, 23, 33,
,而类似的OCaml代码
let myfuns = Array.map (fun clo -> fun arg -> (string_of_int arg) ^ (string_of_int clo)) [|0;1;2;3|];;
(* there is obviously no clo variable here *)
for arg = 0 to 3 do
print_string (myfuns.(arg) arg); print_string ", "
done;;
给出 00, 11, 22, 33,
.
我理解这与应用于 lambda arg: str(arg) + str(clo)
及其对应者 fun arg -> (string_of_int arg) ^ (string_of_int clo)
的不同闭包概念有关。
在 OCaml 中,闭包在创建闭包时将标识符 clo
映射到外部作用域中的变量 clo
的值。在 Python 中,闭包以某种方式包含变量 clo
本身,这说明它受到 for
生成器引起的增量的影响。
这是正确的吗?
这是怎么做到的? clo
变量在全局范围内不存在,我的try
/except
证明了这一点构造。通常,我会假设生成器的变量是它的局部变量,因此不会存在。那么,clo
又在哪里? This answer 给出了关于 __closure__
的见解,但我仍然不完全理解它在生成过程中如何设法引用 clo
变量本身。
另外,除了这种奇怪的行为(对于习惯静态绑定语言的人),还有其他应该注意的注意事项吗?
区别在于python有变量而ocaml有绑定和柯里化。
Python:
myfuns = [lambda arg: str(arg) + str(clo) for clo in range(4)]
for
循环创建一个变量 clo
并在每次迭代中为其分配值 0、1、2、3。 lambda 绑定变量,以便稍后调用 str(clo)
。但是由于循环最后将 3 分配给 clo
所有 lambda 都附加相同的字符串。
Ocaml:
let myfuns = Array.map (fun clo -> fun arg -> (string_of_int arg) ^ (string_of_int clo)) [|0;1;2;3|];;
这里用数组[|0;1;2;3|]
调用Array.map。这将依次评估 fun clo -> ...
绑定 clo
到数组中的每个值。每次绑定都会不同,所以 string_of_int clo
结果也会不同。
虽然这不是唯一的区别,但此部分评估也在 python 中挽救了局面。如果你这样写你的代码:
Python:
def make_lambda(clo):
return lambda arg: str(arg) + str(clo)
myfuns = [make_lambda(clo) for clo in range(4)]
make_lambda 的计算导致 lambda 中的 clo
绑定到 make_lambda 参数的值,而不是 for 循环中的变量。
另一个修复是显式绑定 lambda 中的值:
myfuns = [lambda arg, clo=clo: str(arg) + str(clo) for clo in range(4)]
当 Python 创建闭包时,将所有自由变量收集到 cells 的元组中。由于每个单元格都是可变的,并且 Python 将对单元格的引用传递到闭包中,因此您将在循环中看到归纳变量的最后一个值。让我们看看底层,这是我们的函数,i
在我们的 lambda 表达式中自由出现,
def make_closures():
return [lambda x: str(x) + str(i) for i in range(4)]
这里是这个函数的disassembly
2 0 BUILD_LIST 0
3 LOAD_GLOBAL 0 (range)
6 LOAD_CONST 1 (4)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 21 (to 37)
16 STORE_DEREF 0 (i)
19 LOAD_CLOSURE 0 (i)
22 BUILD_TUPLE 1
25 LOAD_CONST 2 (<code object <lambda>)
28 MAKE_CLOSURE 0
31 LIST_APPEND 2
34 JUMP_ABSOLUTE 13
>> 37 RETURN_VALUE
我们可以看到 16
上的 STORE_DEREF
从堆栈顶部 (TOS) 获取一个正常的整数值,并将其与 STORE_DEREF
一起存储在一个单元格中。接下来的三个命令准备堆栈上的闭包结构,最后 MAKE_CLOSURE
将所有内容打包到闭包中,它表示为单元格的元组(在我们的例子中是 1 元组),
>>> fs = make_closures()
>>> fs[0].__closure__
(<cell at 0x7ff688624f30: int object at 0xf72128>,)
所以它是一个元组,单元格包含一个 int,
>>> fs[0].__closure__[0]
<cell at 0x7ff688624f30: int object at 0xf72128>
>>> type(fs[0].__closure__[0])
cell
这里理解点的关键是自由变量被所有闭包共享,
>>> fs[0].__closure__
(<cell at 0x7f1d63f08b40: int object at 0xf16128>,)
>>> fs[1].__closure__
(<cell at 0x7f1d63f08b40: int object at 0xf16128>,)
由于每个单元格都是对封闭函数作用域中局部变量的引用,实际上,我们可以在 make_closures
函数的 cellvars
属性中找到 i
变量,
>>> make_closures.func_code.co_cellvars
('i',)
因此,我们有一点?整数值通过引用传递并变得可变的令人惊讶的效果。 Python 中的主要惊喜是变量的打包方式以及 for 循环没有自己的作用域。
公平地说,如果您手动创建引用并将其捕获在闭包中,则可以在 OCaml 中获得相同的结果。例如,
let make_closures () =
let arg = ref 0 in
let fs = Array.init 4 (fun _ -> fun _ -> assert false) in
for i = 0 to 3 do
fs.(i) <- (fun x -> string_of_int x ^ string_of_int !arg);
incr arg
done;
fs
所以
let fs = make_closures ()
fs.(1) 1;;
- : string = "14"
历史参考资料
OCaml 和 Python 都受到 Lisp 的影响,并且都暗示了实现闭包的相同技术。令人惊讶的是,结果不同,但不是由于对词法作用域或闭包环境的不同解释,而是由于两种语言的不同对象(数据)模型。
OCaml 数据模型不仅更易于理解,而且由严格的类型系统定义得很好。 Python,由于其动态结构,在对象的解释及其表示方面留下了很大的自由度。因此,在 Python 中,他们决定将变量绑定在闭包的词法上下文中 mutable by default (even if they are integers). See also the PEP-227 以获得更多上下文。
您已经有几个很好的答案,但要关注本质,不同之处在于 Python 做出的两个设计选择:
- 所有变量绑定都是可变的,并在闭包中捕获。
for
comprehensions 不会为每次迭代绑定不同的变量,而是将新值重新分配给同一个变量。
两种设计选择都不是必需的,尤其是后者。例如,在 OCaml 中,for 循环的变量是 not 可变的,而是每次迭代的新绑定。更有趣的是,在 JavaScript、for (let x of ...) ...
中, 会使 x
可变(除非你使用 const
代替),但它仍然是每次迭代分开。这修复了 JavaScript 的旧 for (var x in ...)
的行为,它与 Python 有相同的问题,并且因导致闭包的细微错误而臭名昭著。
这是对 an old answer to a question about the necessity of functools.partial 的一种跟进:虽然这个答案非常清楚地解释了这种现象及其基本原因,但我仍然有一些不清楚的地方。
回顾一下,以下 Python 代码
myfuns = [lambda arg: str(arg) + str(clo) for clo in range(4)]
try :
clo
except NameError :
print("there is no clo")
for arg in range(4) :
print(myfuns[arg](arg), end=", ")
给出03, 13, 23, 33,
,而类似的OCaml代码
let myfuns = Array.map (fun clo -> fun arg -> (string_of_int arg) ^ (string_of_int clo)) [|0;1;2;3|];;
(* there is obviously no clo variable here *)
for arg = 0 to 3 do
print_string (myfuns.(arg) arg); print_string ", "
done;;
给出 00, 11, 22, 33,
.
我理解这与应用于 lambda arg: str(arg) + str(clo)
及其对应者 fun arg -> (string_of_int arg) ^ (string_of_int clo)
的不同闭包概念有关。
在 OCaml 中,闭包在创建闭包时将标识符 clo
映射到外部作用域中的变量 clo
的值。在 Python 中,闭包以某种方式包含变量 clo
本身,这说明它受到 for
生成器引起的增量的影响。
这是正确的吗?
这是怎么做到的? clo
变量在全局范围内不存在,我的try
/except
证明了这一点构造。通常,我会假设生成器的变量是它的局部变量,因此不会存在。那么,clo
又在哪里? This answer 给出了关于 __closure__
的见解,但我仍然不完全理解它在生成过程中如何设法引用 clo
变量本身。
另外,除了这种奇怪的行为(对于习惯静态绑定语言的人),还有其他应该注意的注意事项吗?
区别在于python有变量而ocaml有绑定和柯里化。
Python:
myfuns = [lambda arg: str(arg) + str(clo) for clo in range(4)]
for
循环创建一个变量 clo
并在每次迭代中为其分配值 0、1、2、3。 lambda 绑定变量,以便稍后调用 str(clo)
。但是由于循环最后将 3 分配给 clo
所有 lambda 都附加相同的字符串。
Ocaml:
let myfuns = Array.map (fun clo -> fun arg -> (string_of_int arg) ^ (string_of_int clo)) [|0;1;2;3|];;
这里用数组[|0;1;2;3|]
调用Array.map。这将依次评估 fun clo -> ...
绑定 clo
到数组中的每个值。每次绑定都会不同,所以 string_of_int clo
结果也会不同。
虽然这不是唯一的区别,但此部分评估也在 python 中挽救了局面。如果你这样写你的代码:
Python:
def make_lambda(clo):
return lambda arg: str(arg) + str(clo)
myfuns = [make_lambda(clo) for clo in range(4)]
make_lambda 的计算导致 lambda 中的 clo
绑定到 make_lambda 参数的值,而不是 for 循环中的变量。
另一个修复是显式绑定 lambda 中的值:
myfuns = [lambda arg, clo=clo: str(arg) + str(clo) for clo in range(4)]
当 Python 创建闭包时,将所有自由变量收集到 cells 的元组中。由于每个单元格都是可变的,并且 Python 将对单元格的引用传递到闭包中,因此您将在循环中看到归纳变量的最后一个值。让我们看看底层,这是我们的函数,i
在我们的 lambda 表达式中自由出现,
def make_closures():
return [lambda x: str(x) + str(i) for i in range(4)]
这里是这个函数的disassembly
2 0 BUILD_LIST 0
3 LOAD_GLOBAL 0 (range)
6 LOAD_CONST 1 (4)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 21 (to 37)
16 STORE_DEREF 0 (i)
19 LOAD_CLOSURE 0 (i)
22 BUILD_TUPLE 1
25 LOAD_CONST 2 (<code object <lambda>)
28 MAKE_CLOSURE 0
31 LIST_APPEND 2
34 JUMP_ABSOLUTE 13
>> 37 RETURN_VALUE
我们可以看到 16
上的 STORE_DEREF
从堆栈顶部 (TOS) 获取一个正常的整数值,并将其与 STORE_DEREF
一起存储在一个单元格中。接下来的三个命令准备堆栈上的闭包结构,最后 MAKE_CLOSURE
将所有内容打包到闭包中,它表示为单元格的元组(在我们的例子中是 1 元组),
>>> fs = make_closures()
>>> fs[0].__closure__
(<cell at 0x7ff688624f30: int object at 0xf72128>,)
所以它是一个元组,单元格包含一个 int,
>>> fs[0].__closure__[0]
<cell at 0x7ff688624f30: int object at 0xf72128>
>>> type(fs[0].__closure__[0])
cell
这里理解点的关键是自由变量被所有闭包共享,
>>> fs[0].__closure__
(<cell at 0x7f1d63f08b40: int object at 0xf16128>,)
>>> fs[1].__closure__
(<cell at 0x7f1d63f08b40: int object at 0xf16128>,)
由于每个单元格都是对封闭函数作用域中局部变量的引用,实际上,我们可以在 make_closures
函数的 cellvars
属性中找到 i
变量,
>>> make_closures.func_code.co_cellvars
('i',)
因此,我们有一点?整数值通过引用传递并变得可变的令人惊讶的效果。 Python 中的主要惊喜是变量的打包方式以及 for 循环没有自己的作用域。
公平地说,如果您手动创建引用并将其捕获在闭包中,则可以在 OCaml 中获得相同的结果。例如,
let make_closures () =
let arg = ref 0 in
let fs = Array.init 4 (fun _ -> fun _ -> assert false) in
for i = 0 to 3 do
fs.(i) <- (fun x -> string_of_int x ^ string_of_int !arg);
incr arg
done;
fs
所以
let fs = make_closures ()
fs.(1) 1;;
- : string = "14"
历史参考资料
OCaml 和 Python 都受到 Lisp 的影响,并且都暗示了实现闭包的相同技术。令人惊讶的是,结果不同,但不是由于对词法作用域或闭包环境的不同解释,而是由于两种语言的不同对象(数据)模型。
OCaml 数据模型不仅更易于理解,而且由严格的类型系统定义得很好。 Python,由于其动态结构,在对象的解释及其表示方面留下了很大的自由度。因此,在 Python 中,他们决定将变量绑定在闭包的词法上下文中 mutable by default (even if they are integers). See also the PEP-227 以获得更多上下文。
您已经有几个很好的答案,但要关注本质,不同之处在于 Python 做出的两个设计选择:
- 所有变量绑定都是可变的,并在闭包中捕获。
for
comprehensions 不会为每次迭代绑定不同的变量,而是将新值重新分配给同一个变量。
两种设计选择都不是必需的,尤其是后者。例如,在 OCaml 中,for 循环的变量是 not 可变的,而是每次迭代的新绑定。更有趣的是,在 JavaScript、for (let x of ...) ...
中, 会使 x
可变(除非你使用 const
代替),但它仍然是每次迭代分开。这修复了 JavaScript 的旧 for (var x in ...)
的行为,它与 Python 有相同的问题,并且因导致闭包的细微错误而臭名昭著。