gpt4 book ai didi

python - 我将如何理解Python中的这个深度绑定(bind)示例?

转载 作者:行者123 更新时间:2023-12-02 06:54:22 28 4
gpt4 key购买 nike

Programming Language Pragmatics by Scott

enter image description here


图3.14 Python中的深度绑定。右侧是运行时堆栈的概念图。
封闭中捕获的引用环境显示为虚线框和箭头。什么时候
通过形式参数P调用B,存在两个I实例。因为P的闭包是
在A的初始调用中创建的B的静态链接(实心箭头)指向该A的框架
调用。 B在其print语句中使用该调用的I实例,并且输出为1。

问题在于,正在运行的程序可能具有一个以上在递归子例程中声明的对象的实例。一种语言的闭包
静态作用域在关闭时捕获每个对象的当前实例
被建造。调用闭包的子例程时,即使随后通过递归调用创建了新的实例,它也会找到这些捕获的实例。


因此,基本上,引号试图解释以下程序(与屏幕快照中的程序相同)打印出1

def A(I, P):
def B():
print(I)
# body of A:
if I > 1:
P()
else:
A(2, B)

def C():
pass # do nothing

A(1, C) # main program


我不太了解原因是“因为P的闭包是
在A的初始调用中创建的B的静态链接(实心箭头)指向该先前调用的框架”和“当闭包的子例程被调用时,它将找到这些捕获的实例。”因此,我修改了示例,然后新示例打印出 2而不是 1

def A(I, P):
def B():
print(I)
if I > 2:
P()
elif I > 1:
A(3, B)
else:
A(2, B)

def C():
pass

A(1, C)


另一个修改的示例打印 1

def A(I, P):
def B():
print(I)
if I > 2:
P()
elif I > 1:
A(3, P)
else:
A(2, B)

def C():
pass

A(1, C)


那么我怎么知道哪个关闭很重要呢?

通常,每当一个函数作为参数传递给另一个函数时,都会创建一个闭包吗?

谢谢。

最佳答案

评论已经太久了,因此我将其添加为答案。

我应该说,我是从一个了解其他语言的人的角度回答这个问题的:我现在主要写Python,但是我的术语可能是“错的”(对于它来说,它读的是“正确的,但是后来的语言却像Python弄错了......)。特别是,我故意滑过一堆特定于Python的细节,并避免处理诸如绑定的可变性和Python 3 nonlocal hack之类的事情。

我还认为本书对“深层装订”一词的用法令人困惑,甚至可能是错误的:请参阅结尾处的注释。因此,我在很大程度上忽略了它。

绑定,范围和程度

有三个重要概念。


绑定是名称与其表示的内容之间的关联。最常见的绑定类型是将变量名称与值相关联的变量绑定:还有其他种类(例如,异常类与处理程序之间的绑定,它们由try: ... except: ...构造方法建立),但我只讲变量绑定。
绑定的范围是可访问的代码区域。
绑定的程度是可访问的时间。


范围和范围有几种选择。对于变量绑定,Python具有:


词法作用域,这意味着绑定只能由在源代码中可见的代码访问;
和无限的范围,这意味着只要有可能进行引用就存在绑定。


(另一个常见的范围是动态的:具有动态范围的绑定对于在源代码中可见的任何代码以及从该代码“向下”堆栈的任何代码均可见。另一个常见的范围是确定的,这意味着当控制离开建立它的构造时,绑定就会消失。例如,异常处理程序的绑定在Python中具有动态范围和确定的范围。)

词法作用域的意思是,您(几乎)可以通过阅读源代码来了解一点代码所指的绑定。

所以考虑一下:

def outer():
a = 2
def inner(b):
return a + b
return inner(2)


这里有两个绑定: a绑定在 outer中,而 b绑定在 inner中(实际上,有三个绑定: inner也绑定到了 outer中的函数)。这两个绑定中的每个绑定均被引用一次:在 inner中(而 inner绑定在 outer中被引用一次)。

重要的是,通过阅读代码,您可以知道 a中对 inner的引用是什么:对 outer中建立的绑定。这就是“词法”的意思:您(和编译器,如果足够聪明的话)可以通过查看源代码来知道存在哪些绑定。

这几乎是真的。考虑以下片段:

def maybe(x):
return x + y


maybe中创建了一个绑定,但是有两个引用: y是对不存在的绑定的引用。但是它可能存在:可能存在 y的顶级绑定,这会使此代码起作用。因此,围绕词法绑定有一个特别的警告:存在一个“顶层”环境,所有定义都可以“看到”并且其中可以包含绑定。因此,如果上面的片段被放大以读取

y = 4
def maybe(x):
return x + y


然后这段代码就可以了,因为 maybe可以“看到”顶级环境(实际上,在Python中,这是定义它的模块中的绑定)。

无限范围和一流的功能

对于以上示例,结果在确定范围或不确定范围内将相同。如果您考虑使用Python具有的一流功能,那就不再是事实。情况不再如此,因为函数是通过调用可以引用绑定的对象。考虑一下:

def outer(x):
def inner(y):
return x + y
return inner


这里有三个绑定: outer绑定 xinner,并且 inner绑定 y(并且可以看到 xinner)。因此,现在让 add4 = outer(4)add4(3)应该返回什么(或者等效地, outer(4)(3)应该返回什么)?好吧,答案是 7。之所以如此,是因为 x的绑定存在的时间就可以被引用,或者换句话说,由于 inner的任何实例存在,它都存在,因为它们引用了它。这就是“无限范围”的意思。

(如果Python仅具有确定的范围,则 outer(4)(3)将是某种错误,因为它会引用不再存在的绑定。仅具有确定的范围的语言实际上无法以任何有用的方式具有一流的功能。)

重要的是要理解的是,词法作用域告诉您哪些绑定可见,但是这些绑定的实际实例当然是动态的。因此,如果您考虑这样的事情:

def adder(n):
return lambda e: e + n

a1 = adder(12)
a2 = adder(15)


然后 a1a2引用 n的不同绑定: a1(0)12,而 a2(0)15。因此,通过阅读源代码,您可以知道捕获了哪些绑定,但是您需要运行它来了解捕获了哪些绑定-换句话说,变量的值是多少。

与此相比:

def signaller(initial):
s = [initial]
def setter(new):
s[0] = new
return new
def getter():
return s[0]
return (setter, getter)

(str, gtr) = signaller(0)


现在, strgtr捕获了 s的相同绑定,因此 str(1)将导致 gtr()返回 1

关闭

所以这实际上就是所有要知道的。除了人们使用某些特殊术语外,特别是术语“关闭”。

闭包只是一个函数,它引用超出其自身定义的某些词法绑定。据说这种功能“封闭”了这些结合。

我认为这是一个很好的问题,问为什么根本不需要这个词?您真正需要了解的是范围规则,所有其他内容都从该范围规则中得出:为什么需要这个特殊术语?我认为原因部分是历史原因,部分是实用原因:


从历史上看,对于所有封闭的绑定而言,封闭都会带来很多额外的负担,因此人们对某个功能是否是封闭很感兴趣。
在实用上,闭包的行为可能取决于闭包的绑定,而闭包以稍微不明显的方式(请参见上面的示例),这也许可以证明该术语是正确的。


示例代码

示例代码是

def A(I, P):
def B():
print(I)
# body of A:
if I > 1:
P()
else:
A(2, B)

def C():
pass # do nothing

A(1, C) # main program


因此,在调用 A时,有一个本地函数 B可以看到 I的绑定(还有 PB本身,但它没有引用这些绑定,因此可以忽略)他们)。每次对 A的调用都会为 IPB创建新的绑定,并且这些绑定对于每个调用都是不同的。这包括递归调用,这是在这里使您感到困惑的技巧。

那么, A(1, C)是做什么的?


它将 I绑定到 1,并将 B绑定到一个可以看到 I绑定的闭包。它还将 P绑定到 C的全局(模块)值,该值是一个函数,但没有任何内容引用此绑定。
然后,它使用 I的参数和 1的值(刚创建的闭包)递归调用自身(因为 2B)。
在递归调用中,有新的绑定: I现在绑定到 2,并且 P绑定到外部调用的闭包。
创建一个新的闭包,该闭包捕获这些绑定,并在此内部调用中将自身绑定到 B。没有任何内容涉及此绑定。
然后调用绑定到 P的闭包。它是在外部调用中创建的闭包,对于 I可以看到的绑定是对其可见的绑定,其值为 1。这样它打印 1,我们就完成了。


您可以通过更改定义以打印一些有用的信息来查看正在发生的情况:

from __future__ import print_function # Python 2

def A(I, P):
def B():
print(I)
print("{I} {P} {B}".format(I=I, P=P, B=B))
if I > 1:
P()
else:
A(2, B)

def C():
pass

A(1, C)


打印(例如):

1 <function C at 0x7f7a03768e60> <function B at 0x7f7a03768d70>
recursing with (2, <function B at 0x7f7a03768d70>)
2 <function B at 0x7f7a03768d70> <function B at 0x7f7a03651a28>
calling <function B at 0x7f7a03768d70>
1


请注意,在内部调用中,有两个将自己标识为 B的函数:其中一个与在外部调用中创建的函数相同(将被调用),而另一个是刚刚创建的函数闭包,将不再引用。



深度绑定

我认为该书对“深度绑定”一词的用法充其量是令人困惑的,实际上可能是完全错误的:该词可能已经改变了含义,但最初并不一定意味着该书的含义。

术语“深度绑定”和“浅绑定”是指具有动态范围的语言的实现策略。在具有动态作用域的系统中,通过动态搜索调用堆栈直至找到绑定的“自由”变量引用(未由特定代码位绑定)。因此,在具有动态绑定的语言中,您无法通过看一点代码来判断它可以看到的绑定(编译器也看不到!),因为它可以看到的绑定取决于当前调用堆栈的外观它运行。

动态绑定对于异常处理程序之类的东西非常有用,但是对于大多数变量绑定而言通常很糟糕。

它之所以如此糟糕,是因为天真的实现技术使其固有的速度变慢,并且聪明的实现技术必须非常聪明才能在具有多个控制线程的系统中工作。

深度绑定是幼稚的实现技术。在深度绑定中,变量访问按照您的思维方式工作:系统搜索堆栈,直到找到要查找的名称的绑定,然后使用该名称。如果堆栈很深并且绑定距离很长,则速度很慢。

浅绑定是一种聪明的实现技术。这是通过以下方式进行的,而不是将当前绑定存储在堆栈中,而不是将先前的值存储在堆栈中,并将当前值粉碎到与变量名关联的插槽中,该插槽始终位于同一位置。因此,现在查找绑定仅涉及查找名称:没有搜索。但是创建和销毁绑定可能会变慢,因为需要保留旧值。此外,在存在多个控制线程的情况下,浅表绑定显然不是安全的:如果所有线程共享用于绑定的插槽,则灾难将随之而来。因此,每个线程都需要有自己的插槽,或者插槽需要按线程索引以及以某种方式命名。

我怀疑,对于使用动态范围的情况(例如异常处理程序),系统会使用深度绑定,因为在多线程系统中更容易正确实现,并且性能并不关键。

Here是亨利·贝克(Henry Baker)撰写的有关深浅装订的经典早期论文。

关于python - 我将如何理解Python中的这个深度绑定(bind)示例?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45872572/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com