Python命名与绑定

在Python中,作用域定义了一个代码块中名称的可见性。如果代码块中定义了一个局部变量,则其作用域包含该代码块。如果定义发生在函数代码块中,则其作用域会扩展到该函数所包含的任何代码块,除非有某个被包含代码块引入了对该名称的不同绑定。

一、名称的绑定

名称是通过名称绑定操作来引入的,用于指代对象。

以下是一些常见的名称绑定结构:

1、函数的正式参数。

2、类定义。

3、函数定义。

4、赋值表达式。

5、在赋值语句中作为标识符的目标。

  • for循环头;
  • 在with语句、except子句、except*子句或格式化模式匹配的as模式的as之后;
  • 在结构模式匹配中的捕获模式。

6、import语句。

7、type语句。

8、类型形参列表。

形式为 from ... import * 的 import 语句绑定所有在导入的模块中定义的名字,除了那些以下划线开头的名字,但是这种形式只能在模块级别上使用。另外,del 语句的目标也被视作一种绑定(虽然其实际语义为解除名称绑定)。

每条赋值或导入语句均发生于类或函数内部定义的代码块中,或是发生于模块层级(即最高层级的代码块)。如果名称绑定在一个代码块中,则为该代码块的局部变量,除非声明为 nonlocal 或 global;如果名称绑定在模块层级,则为全局变量。 (模块代码块的变量既为局部变量又为全局变量。) 如果变量在一个代码块中被使用但不是在其中定义,则为 自由变量。

二、名称的解析

作用域定义了一个代码块中名称的可见性。 如果代码块中定义了一个局部变量,则其作用域包含该代码块;如果定义发生于函数代码块中,则其作用域会扩展到该函数所包含的任何代码块,除非有某个被包含代码块引入了对该名称的不同绑定。

当一个名称在代码块中被使用时,会由包含它的最近作用域来解析。 对一个代码块可见的所有这种作用域的集合称为该代码块的环境。

当一个名称完全找不到时,将会引发 NameError 异常。 如果当前作用域为函数作用域,且该名称指向一个局部变量,而此变量在该名称被使用的时候尚未绑定到特定值,将会引发 UnboundLocalError 异常,UnboundLocalError 为 NameError 的一个子类。

如果一个代码块内的任何位置发生名称绑定操作,则代码块内所有对该名称的使用都会被视为对当前代码块的引用。 当一个名称在其被绑定前就在代码块内被使用时将会导致错误, 这个规则是很微妙的,Python 缺少声明语法并且允许名称绑定操作发生于代码块内的任何位置。 一个代码块的局部变量可通过在整个代码块文本中扫描名称绑定操作来确定。

如果在一个代码块中使用了 global 语句,那么在该代码块中对该语句所指定名称的所有引用都将在最高层级命名空间内进行,这些引用将首先在包含该代码块的模块的命名空间中查找,然后才是内置命名空间(即 builtins 模块)。如果在最高层级命名空间中找不到指定的名称,搜索将继续在内置命名空间中进行。global 语句必须位于所有对其所列出名称的使用之前。

global 语句与同一代码块中名称绑定具有相同的作用域。 如果一个自由变量的最近包含作用域中有一条 global 语句,则该自由变量也会被当作是全局变量。

nonlocal 语句会使得相应的名称指向之前在最近包含函数作用域中绑定的变量。 如果指定的名称不存在于任何包含函数作用域中则将在编译时引发 SyntaxError。 类型形参 不能使用 nonlocal 语句来重新绑定。

模块的作用域会在模块第一次被导入时自动创建。 一个脚本的主模块总是被命名为 __main__。

类定义代码块以及传给 exec() 和 eval() 的参数是名称解析的上下文中的特殊情况。 类定义是可能使用并定义名称的可执行语句, 这些引用遵循正常的名称解析规则,例外之处在于未绑定的局部变量会在全局命名空间中查找。

类定义的命名空间会成为该类的属性字典, 在类代码块中定义的名称的作用域会被限制在类代码块中;它不会扩展到方法的代码块中, 这包括推导式和生成器表达式,但不包括标注作用域,因为它可以访问所包含的类作用域。 这意味着以下代码将会失败:

class A:
a = 42
b = list(a + i for i in range(10))

但是,下面的代码将会成功:

class A:
type Alias = Nested
class Nested: pass
print(A.Alias.__value__) # <type 'A.Nested'>

三、标注作用域

类型形参列表 和 type 语句引入了标注作用域,其行为很像函数作用域,但具有下述的几处例外。 标注目前没有使用标注作用域,但它们预期会在实现了 PEP 649 的 Python 3.13 中使用标注作用域。

标注作用域将在下列情况中使用:

  • 针对泛型类型别名的类型形参列表;
  • 针对 泛型函数 的类型形参列表。 泛型函数的标注会在标注作用域内执行,但其默认值和装饰器则不会;
  • 针对 泛型类 的类型形参列表。 泛型类的基类和关键字参数会在标注作用域内执行,但其装饰器则不会;
  • 针对类型变量的绑定和约束 (惰性求值);
  • 类型别名的值 (惰性求值)。

标注作用域在以下几个方面不同于函数作用域:

  • 标注作用域能够访问其所包含的类命名空间。 如果某个标注作用域紧接在一个类作用域之内,或是位于紧接一个类作用域的另一个标注作用域之内,则该标注作用域中的代码将能使用在该类作用域中定义的名称,就像它是在该类内部直接执行一样, 这不同于在类中定义的常规函数,后者无法访问在类作用域中定义的名称;
  • 标注作用域中的表达式不能包含 yield, yield from, await 或 := 表达式;(这些表达式在包含于标注作用域之内的其他作用域中则是允许的。)
  • 在标注作用域中定义的名称不能在内部作用域中通过 nonlocal 语句来重新绑定。 这只包括类型形参,因为没有其他可以在标注作用域内部出现的语法元素能够引入新的名称;
  • 虽然标注作用域具有其内部名称,但该名称不会在作用域内部定义对象的 __qualname__ 中反映出来。 这些对象的 __qualname__ 就像他们是定义在包含作用域中的对象一样。

3.12 新版功能: 标注作用域是在 Python 3.12 中作为 PEP 695 的一部分引入的。

四、惰性求值

通过使用 type 语句创建的类型别名的值将被惰性求值。这意味着在类型别名或类型变量创建时,它们不会被立即求值。相反,它们只有在需要处理属性访问时才会被求值。这种特性也适用于通过类型形参语法创建的类型变量的绑定和约束。

示例:

>>>type Alias = 1/0
>>>Alias.__value__
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>>def func[T: 1/0](): pass
>>>T = func.__type_params__[0]
>>>T.__bound__
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

此处的异常只有在类型别名的 __value__ 属性或类型变量的 __bound__ 属性被访问时才会被引发。此行为主要适用于当创建类型别名或类型变量时对尚未被定义的类型进行引用。 例如,惰性求值将允许创建相互递归的类型别名:

from typing import Literal
type SimpleExpr = int | Parenthesized
type Parenthesized = tuple[Literal["("], Expr, Literal[")"]]
type Expr = SimpleExpr | tuple[SimpleExpr, Literal["+", "-"], Expr]

被惰性求值的值是在标记作用域内进行求值的,出现在被惰性求值的值内部的名称的查找范围就相当于它们是在紧邻的作用域中被使用。

五、内置命名空间和受限执行

CPython 实现细节: 用户不应该接触 __builtins__,严格说来它属于实现细节。 用户如果要重载内置命名空间中的值则应该 import builtins 并相应地修改该模块中的属性。

与一个代码块的执行相关联的内置命名空间实际上是通过在其全局命名空间中搜索名称 __builtins__ 来找到的;这应该是一个字典或一个模块(在后一种情况下会使用该模块的字典)。 默认情况下,当在 __main__ 模块中时,__builtins__ 就是内置模块 builtins;当在任何其他模块中时,__builtins__ 则是 builtins 模块自身的字典的一个别名。

六、与动态特性交互

自由变量的名称解析发生于运行时而不是编译时。 这意味着以下代码将打印出 42:

i = 10
def f():
print(i)
i = 42
f()

exec() 函数和 eval() 函数在解析名称时,没有对完整环境的访问权限。这意味着名称可以在调用者的局部和全局命名空间中被解析。自由变量的解析不是在最近包含命名空间中,而是在全局命名空间中。exec() 和 eval() 函数有可选参数用来重载全局和局部命名空间。如果只指定一个命名空间,则它会同时作用于两者。

广告合作
QQ群号:707632017

温馨提示:

1、本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com。(#改为@)

2、本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。

目录