松散的后期装订与严格的后期装订

Loose late binding v. strict late binding

提问人:Géry Ogam 提问时间:8/8/2020 最后编辑:Géry Ogam 更新时间:8/10/2020 访问量:536

问:

在阅读 Python 的执行模型文档时,我意识到 Python 的自由变量似乎没有严格的后期绑定属性,其中任何代码块中出现的名称绑定都可用于名称解析。事实上,执行:

def f():
    return x

def g():
    x = 0
    return f()

print(g())

提高:

NameError: name 'x' is not defined

它们具有相当松散的后期绑定属性,其中只有在引入自由变量的代码块的外部代码块中发生的名称绑定才能用于名称解析。确实执行

def f():
    return x

x = 0
print(f())

指纹:

0

与严格的后期绑定属性相比,松散的后期绑定属性的优点和缺点是什么?

python 语言设计 后期绑定 自由变量

评论

0赞 Tabaene Haque 8/8/2020
这是你要找的东西吗?- stackoverflow.com/questions/367411/....
5赞 MisterMiyagi 8/8/2020
您指的是动态范围还是词法/静态范围
1赞 MisterMiyagi 8/8/2020
请注意,人们始终可以通过携带命名空间或上下文化全局命名空间来模拟动态范围(例如,参见 contextvarsthreading.local)。
0赞 Géry Ogam 8/8/2020
@MisterMiyagi 没错!我不敢相信我以前从未听说过这个基本概念!这清除了我脑海中的很多事情。整篇文章都是一颗宝石。非常感谢。它甚至将早期绑定定义为静态范围,将后期绑定定义为动态范围。因此,我错误地将术语“后期绑定”用于 Python,因为它具有静态范围。你能写一个答案吗,我很乐意接受吗?

答:

-2赞 Tabaene Haque 8/8/2020 #1

静态(早期)和动态(晚期)绑定:

绑定是指程序文本中的名称与它们所引用的存储位置的关联。在静态绑定中,此关联是在生成时预先确定的。使用动态绑定时,直到运行时才能确定此关联。

动态绑定是发生在 Python 中的绑定。这意味着 Python 解释器仅在代码运行时进行绑定。例如-

>>> if False:
...     x  # This line never runs, so no error is raised
... else:
...     1 + 2
...
3
>>>

动态绑定的优点

  • 动态类型绑定的主要优点是灵活性。编写通用代码更容易。
    • 例如 - 使用动态类型绑定的语言处理数据列表的程序可以编写为通用程序。

动态绑定的缺点

  • 编译器的错误检测能力减弱。编译器可能捕获的一些错误。
  • 运行时开销相当可观。

评论

1赞 Géry Ogam 8/8/2020
对不起,您谈论的是打字,这与绑定是一个完全不同的话题。
4赞 MisterMiyagi 8/10/2020 #2

这通常称为动态范围和静态范围。粗略地说,动态范围通过调用嵌套确定范围,静态范围通过声明嵌套确定范围。

通常,对于任何具有调用堆栈的语言,动态范围都非常容易实现 - 名称查找只是线性搜索当前堆栈。相比之下,静态范围更为复杂,需要多个具有自身生存期的不同作用域。

但是,静态范围通常更容易理解,因为变量的作用域永远不会改变 - 名称查找必须解析一次,并且始终指向相同的作用域。相比之下,动态作用域更脆弱,在调用函数时,名称在不同的范围内解析或没有作用域。


Python 的范围规则主要由引入嵌套范围(“闭包”)的 PEP 227 和引入可写嵌套范围的 PEP 3104 定义。这种静态作用域的主要用例是允许高阶函数(“函数-生成-函数”)自动参数化内部函数;这通常用于回调、装饰器或工厂函数。nonlocal

def adder(base=0):  # factory function returns a new, parameterised function
    def add(x):
        return base + x  # inner function is implicitly parameterised by base
    return add

这两个 PEP 都编码了 Python 如何处理静态范围的复杂性。具体来说,范围在编译时被解析一次——此后每个名称都严格地要么是全局的,要么是非本地的,要么是本地的。作为回报,静态范围允许优化变量访问——变量可以从快速局部数组、闭包单元格的间接数组或慢速全局字典中读取变量。

这种静态范围名称解析的一个伪特征是:名称可能在本地限定范围,但尚未在本地分配。即使某为该名称分配了一些值,静态范围也会禁止访问它。UnboundLocalError

>>> some_name = 42
>>> def ask():
...     print("the answer is", some_name)
...     some_name = 13
...
>>> ask()
UnboundLocalError: local variable 'some_name' referenced before assignment

有各种方法可以规避这一点,但它们都归结为程序员必须明确定义如何解析名称。


虽然 Python 本身没有实现动态范围,但它可以很容易地被模拟。由于动态作用域与每个调用堆栈的作用域堆栈相同,因此可以显式实现。

Python 原生提供 threading.local,用于将变量上下文化到每个调用堆栈。类似地,contextvars 允许显式地将变量上下文化——这对于例如 代码,它避开了常规调用堆栈。线程的朴素动态作用域可以构建为线程本地的文本作用域堆栈:async

import contextlib
import threading


class DynamicScope(threading.local):  # instance data is local to each thread
    """Dynamic scope that supports assignment via a context manager"""
    def __init__(self):
        super().__setattr__('_scopes', [])  # keep stack of scopes

    @contextlib.contextmanager  # a context enforces pairs of set/unset operations
    def assign(self, **names):
        self._scopes.append(names)  # push new assignments to stack
        yield self                  # suspend to allow calling other functions
        self._scopes.pop()          # clear new assignments from stack

    def __getattr__(self, item):
        for sub_scope in reversed(self._scopes):  # linearly search through scopes
            try:
                return sub_scope[item]
            except KeyError:
                pass
        raise NameError(f"name {item!r} not dynamically defined")

    def __setattr__(self, key, value):
        raise TypeError(f'{self.__class__.__name__!r} does not support assignment')

这允许全局定义一个动态范围,一个名称可以在有限的持续时间内被编辑到这个范围。分配的名称在调用的函数中自动可见。assign

scope = DynamicScope()

def print_answer():
    print(scope.answer)  # read from scope and hope something is assigned

def guess_answer():
    # assign to scope before calling function that uses the scope
    with scope.assign(answer=42):
        print_answer()

with scope.assign(answer=13):
    print_answer()  # 13
    guess_answer()  # 42
    print_answer()  # 13
print_answer()      # NameError: name 'answer' not dynamically defined

评论

0赞 Géry Ogam 8/10/2020
感谢 PEP 227 参考和动态示波器仿真奖励!只有一件事我没有得到:“这种静态范围名称解析的人工制品是” Javascript 的行为相同。但是,C++ 和 Scheme 也是静态作用域的,但它们不会为使用在封闭块中绑定但稍后在本地块中绑定的名称的函数引发未绑定的局部变量错误。你提到这一点是件好事,因为我今天早上在这篇文章中问了一个问题:可变吊装的动机是什么?UnboundLocalError
1赞 MisterMiyagi 8/10/2020
如果我能挖掘出一些可靠的来源,我会写一个问题的答案。
1赞 Géry Ogam 8/18/2020
我在官方 Python 论坛上问了关于可变提升动机的问题,最后 Guido 自己给出了理由。如果您有兴趣,请在此处发布帖子。
1赞 MisterMiyagi 2/25/2021
@SkyWalker 你可以做.您也可以通过更改 来将当前作用域视为可变的,这样就可以了。with scope.assign(flop_count=scope.flop_count + 10):__setattr__scope.flop_count += 10
1赞 MisterMiyagi 2/25/2021
@SkyWalker 它是一堆命名空间,所以是一个“字典列表”——我看不出“平面字典”或“字典字典”如何表达这一点。