0

While reading Python’s Execution model documentation, I realized that Python’s free variables do not seem to have a strict late binding property where a name binding occurring in any code block can be used for name resolution. Indeed, executing:

def f():
    return x

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

print(g())

raises:

NameError: name 'x' is not defined

They have rather a loose late binding property where only a name binding occurring in an outer code block of the code block introducing the free variable can be used for name resolution. Indeed executing

def f():
    return x

x = 0
print(f())

prints:

0

What are the benefits and drawbacks of the loose late binding property compared to the strict late binding property?

4

2 回答 2

4

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

一般来说,对于任何具有调用堆栈的语言,动态作用域都很容易实现——名称查找只是线性地搜索当前堆栈。相比之下,静态作用域更复杂,需要几个不同的作用域和它们自己的生命周期。

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


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

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
于 2020-08-10T12:35:31.077 回答
-2

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

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

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

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

动态绑定的优点

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

动态绑定的缺点

  • 编译器的错误检测能力被削弱。编译器可能已经捕获的一些错误。
  • 运行时的大量开销。
于 2020-08-08T12:17:46.840 回答