2

这是对有关 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变量在全局范围内不存在。一般来说,我会假设生成器的变量是本地的,因此无法生存。那么,再次,在哪里?这个答案提供了一些见解,但我仍然不完全理解它是如何在生成期间设法引用变量本身的。tryexceptclo__closure__clo

此外,除了这种奇怪的行为(对于习惯于静态绑定语言的人),还有其他需要注意的警告吗?

4

3 回答 3

2

当 Python 创建闭包时,会将所有自由变量收集到一个元组。由于每个单元格都是可变的,并且 Python 将对单元格的引用传递到闭包中,因此您将在循环中看到归纳变量的最后一个值。让我们看看底层,这是我们的函数,i在我们的 lambda 表达式中自由出现,

def make_closures():
    return [lambda x: str(x) + str(i) for i in range(4)]

这是这个函数的反汇编

  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        

我们可以看到STORE_DEREFon16从栈顶 (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>,)

由于每个单元格都是对封闭函数范围内的局部变量的引用,实际上,我们可以在函数中找到该i变量,在属性中,make_closurescellvars

>>> 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 中,他们决定在默认情况下使绑定在闭包的词法上下文中的变量可变(即使它们是整数)。另请参阅PEP-227了解更多上下文。

于 2019-08-05T14:01:25.410 回答
1

区别在于 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|];;

在这里,您使用数组调用 Array.map [|0;1;2;3|]。这将依次评估与数组中每个值的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 的求值导致clolambda 中的 被绑定到 make_lambda 参数的值,而不是 for 循环中的变量。

另一个解决方法是显式绑定 lambda 中的值:

myfuns = [lambda arg, clo=clo: str(arg) + str(clo) for clo in range(4)]
于 2019-08-05T13:05:34.293 回答
1

您已经有了几个很好的答案,但要关注本质,区别在于 Python 做出的两个设计选择:

  1. 所有变量绑定都是可变的,并且在闭包中被捕获。
  2. for推导式不会为每次迭代绑定不同的变量,而是将新值重新分配给相同的变量。

两种设计选择都不是必需的,尤其是后者。例如,在 OCaml 中,for 循环的变量不是可变的,而是每次迭代的新绑定。更有趣的是,在 JavaScript 中,它for (let x of ...) ... 变得x可变(除非你改用const它),但它对于每次迭代仍然是独立的。这修复了 JavaScript 的旧版本的行为for (var x in ...),它与 Python 存在相同的问题,并且因导致闭包的细微错误而臭名昭著。

于 2019-08-14T08:56:50.850 回答