我该如何理解Python中这个深度绑定的例子?

从编程语言语用学作者Scott,

在这里输入图像描述

图3.14 Python中的深度绑定。 右边是运行时栈的概念视图。 在闭包中捕获的参考环境显示为虚线框和箭头。 当通过形式参数P调用B时,存在两个I的实例。 因为P的闭包是在A的初始调用中创建的,所以 B的静态链接(实线箭头)指向该早期调用的框架。 B在其打印语句中使用该调用的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攻击等问题。

我也认为这本书对“深度绑定”这个术语的使用很混乱,也许是错误的:请参阅结尾处的注释。 因此,我很大程度上忽视了它。

绑定,范围和程度

有三个重要的概念。

  • 绑定是名称与其所表示的内容之间的关联。 最常见的一种绑定是一种变量绑定,它将一个变量名与一个值相关联:还有其他类型(例如,通过try: ... except: ... constructs建立的异常类和处理程序之间的绑定)但我只会谈论变量绑定。
  • 绑定的范围是它可访问的代码的区域。
  • 绑定的范围是可以访问多长时间。
  • 范围和范围有几个选项。 对于变量绑定,Python有:

  • 词法范围,这意味着绑定只能在源代码中可见的代码中访问;
  • 无限期的范围,这意味着只要有任何可能性参照就存在约束力。
  • (另一个常见的作用域是动态的 :对于在源代码中可见的任何代码以及从该代码'向下堆栈'的任何代码,动态作用域的绑定是可见的。另一个常见范围是明确的 ,这意味着绑定消失了,控制权离开了建立它的构造,异常处理程序的绑定在Python中具有动态范围和确定范围)。

    词汇范围意味着你可以(几乎)通过阅读源代码来看出代码所涉及的是什么。

    所以考虑一下:

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

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

    而且重要的是,通过阅读代码,你可以告诉引用ainner是:它是在绑定建立outer 。 这就是'词汇'的意思:你(和编译器,如果它足够聪明)可以通过查看源代码来判断存在哪些绑定。

    这只是真的。 考虑这个片段:

    def maybe(x):
        return x + y
    

    有绑定创建一个,在maybe的,但两个引用: x是哪个不知道存在有约束力的参考。 但它可能存在:可能有一个顶级绑定的x ,这将使此代码工作。 所以对词汇绑定有一个特别的警告:有一个“顶级”环境,所有定义可以“看到”并且可以包含绑定。 所以,如果上面的片段被放大阅读

    x = 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引用na1(0)不同绑定为12a2(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的全局(模块)值, C是一个函数,但没有指向此绑定。
  • 然后它自己递归地调用自己(因为I1 ),参数为2B的值,这是刚刚创建的闭包。
  • 在递归调用中,有新的绑定: I现在绑定到2P从外部调用绑定到闭包。
  • 创建一个新的闭包,捕获这些绑定,并在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 :它们中的一个与在外部调用中创建的函数相同(将被调用),而另一个函数是刚刚创建的闭包,这是从来没有再次引用。


    深度绑定

    我认为这本书对“深度约束”一词的使用最多是混淆,实际上可能是彻头彻尾的错误:这个词有可能改变了含义,但它当然并不意味着本书认为它的意思。

    术语“深度绑定”和“浅层绑定”是指具有动态范围的语言的实现策略。 在具有动态范围的系统中,通过动态搜索调用堆栈直到找到它的绑定为止,查找'空闲'变量引用(不受特定位代码限制的引用)。 因此,在使用动态绑定的语言中,通过查看一些代码可以看出它能够看到什么样的绑定(而编译器也不行),因为它可以看到的绑定取决于当前调用堆栈的样子它运行。

    动态绑定非常适用于异常处理程序,但通常对于大多数可变绑定来说都很糟糕。

    可怕的一个原因是,一个天真的实现技术使得它本身就很慢,而一个聪明的实现技术需要在具有多个控制线程的系统中工作。

    深度绑定是一种天真的实现技术。 在深度绑定中,变量访问按照您的想法工作:系统搜索堆栈,直到找到要查找的名称的绑定,然后使用它。 如果堆叠很深并且绑定距离很远,则速度很慢。

    浅绑定是聪明的实现技术。 这样做的效果不是将当前绑定存储在堆栈中,而是将先前的值存储在堆栈中,并将当前值砸成与始终位于相同位置的变量名称关联的槽。 所以现在查找绑定只需要查找名称:没有搜索。 但是创建和销毁绑定可能会比较慢,因为旧值需要隐藏起来。 此外,浅层绑定在多线程控制的情况下并不明显:如果所有线程共享绑定的槽,那么灾难将随之而来。 因此,每个线程需要有自己的插槽,或插槽需要通过线索以及名称进行索引。

    我怀疑,对于使用动态范围的情况,例如异常处理程序,系统使用深度绑定,因为在多线程系统中正确运行并且性能并不重要。

    这是亨利贝克提出的关于深与浅结合的经典早期论文。

    链接地址: http://www.djcxy.com/p/51127.html

    上一篇: How shall I understand this deep binding example in Python?

    下一篇: Scope of variables in function factories