3-2-作用域

1. Python 作用域基础

在一个程序中使用变量名时,Python 创建、改变或查找变量名都是在所谓的命名空间(一个保存变量名的地方)中进行的。代码中变量名被赋值的位置决定了这个变量名能被访问到的范围。

一个函数的所有变量名都是与函数的命名空间相关联的:

  • 一个在 def 内定义的变量名能够被 def 内的代码使用。不能在函数的外部引用这样的变量名。
  • def 之中的变量名与 def 之外的变量名并不冲突。

变量可以在三个不同的地方分配,分别对应三种不同的作用域:

  • 如果一个变量在 def 内赋值,它被定位在这个函数之内。
  • 如果一个变量在一个嵌套的 def 中赋值,对于嵌套的函数来说,它是非本地的。
  • 如果在 def 之外赋值,它就是整个文件全局的。

1.1 作用域法则

函数定义了本地作用域,模块定义的是全局作用域。

  • 内嵌的模块是全局作用域。
  • 全局作用域的作用范围仅限于单个文件。
  • 每次对函数的调用都创建了一个新的本地作用域。
  • 赋值的变量名除非声明为全局变量或非本地变量,否则均为本地变量。
  • 所有其他的变量名都可以归纳为本地、全局或者内置的。

1.2 变量名解析:LEGB原则

对于一个 def 语句:

  • 在默认情况下,变量名赋值会创建或者改变本地变量。
  • 变量名引用分为四个作用域进行查找:首先是本地,之后是函数内,之后全局,最后是内置。
  • global 声明和 nonlocal 声明将赋值的变量名映射到模块和函数内部的作用域。

Python 的变量名解析机制有时称为 LEGB 法则。

  • 当在函数中使用未认证的变量名时,Python 搜索四个作用域 [本地作用域(L),之后是上一层结构中 def 或 lambda 的本地作用域(E),之后是全局作用域(G),最后是内置作用域(B)] 并且在第一处能够找到这个变量名的地方停下来。
  • 当在函数中给一个变量名赋值时(而不是在一个表达式中对其进行引用),Python 总是创建或改变本地作用域的变量名,除非它已经在那个函数中声明为全局变量。
  • 当在函数之外给一个变量名赋值时,本地作用域与全局作用域(这个模块的命名空间)是相同的。

其他 Python 作用域:预览
从技术上讲,Python 中还有三个作用域——一些解析中的临时循环变量,一些 try 句柄中的异常引用变量和类语句中的局部作用域。

大多数语句块和其他构造不本地化它们中使用的变量名,除了以下特定版本的情况(其变量不可用,但也不会与周围的代码发生冲突):

  • **解析变量。**变量 X 用于在解析表达式中引用当前迭代项,如[X for X in I]。在 Python 3.X 中,因为它们可能会与其他名称发生冲突,并反映生成器中的内部状态,因此这些变量在表达式本身的所有解析形式(生成器、列表、集合和字典)中都是本地的。
  • **异常变量。**变量 X 用于在 try 语句处理程序子句中引用被抛出的异常,例如 except E as X。因为它们可能会延迟垃圾收集的内存恢复,在 Python 3.X 中,这些变量都是本地的,而且当块退出时(即使你在代码前面使用过它),也会被删除。

类语句也为在其块的顶层赋值的变量名创建了一个新的本地作用域。对于 def 来说,在类中赋值的变量名与其他地方的变量名不冲突,并遵循 LEGB 查找规则,类块是“L”级别。与模块和导入一样,这些变量名在类语句结束后也会转变为类对象属性。

与函数不同的是,类名不是每次调用都创建的:类对象调用生成实例,实例继承类中赋值的变量名,并将每个对象状态记录为属性。虽然 LEGB 规则是用来解析在类本身的顶层以及嵌套在其中的方法函数的顶层中使用的变量名,类本身被作用域查找跳过——它们的变量名必须是作为对象属性被获取。由于 Python 搜索包含的函数以查找引用的变量名,而不搜索包含的类,所以 LEGB 规则仍然适用于 OOP 代码。

1.3 作用域实例

1
2
3
4
5
6
7
8
9
# 全局作用域
X = 99 # X 和 func 在模块中赋值:全局

def func(Y): # Y 和 Z 在函数中赋值:局部
# 局部作用域
Z = X + Y # X 是全局变量
return Z

func(1)
100

全局变量名:X,func

X 是在模块文件顶层注册的,所以它是全局变量;它能够在函数内部进行引用而不需要特意声明为全局变量。因为同样的原因 func 也是全局变量;def 语句在这个模块文件顶层将一个函数对象赋值给了变量名 func。

本地变量名:Y,Z

Y 和 Z 是本地变量(并且只在函数运行时存在),因为它们都是在函数定义内部进行赋值的:Z 是通过 = 语句赋值,Y 是由于参数总是通过赋值来进行传递的。

1.4 内置作用域

内置作用域仅仅是一个名为 builtins 的内置模块,但是必须要导入 builtins 之后才能使用内置作用域。

1
2
import builtins
dir(builtins)
['ArithmeticError',
 'AssertionError',
 'AttributeError',
 'BaseException',
 'BlockingIOError',
 'BrokenPipeError',
 'BufferError',
 'BytesWarning',
 'ChildProcessError',
 'ConnectionAbortedError',
 'ConnectionError',
 'ConnectionRefusedError',
 'ConnectionResetError',
 'DeprecationWarning',
 'EOFError',
 'Ellipsis',
 'EnvironmentError',
 'Exception',
 'False',
 'FileExistsError',
 'FileNotFoundError',
 'FloatingPointError',
 'FutureWarning',
 'GeneratorExit',
 'IOError',
 'ImportError',
 'ImportWarning',
 'IndentationError',
 'IndexError',
 'InterruptedError',
 'IsADirectoryError',
 'KeyError',
 'KeyboardInterrupt',
 'LookupError',
 'MemoryError',
 'ModuleNotFoundError',
 'NameError',
 'None',
 'NotADirectoryError',
 'NotImplemented',
 'NotImplementedError',
 'OSError',
 'OverflowError',
 'PendingDeprecationWarning',
 'PermissionError',
 'ProcessLookupError',
 'RecursionError',
 'ReferenceError',
 'ResourceWarning',
 'RuntimeError',
 'RuntimeWarning',
 'StopAsyncIteration',
 'StopIteration',
 'SyntaxError',
 'SyntaxWarning',
 'SystemError',
 'SystemExit',
 'TabError',
 'TimeoutError',
 'True',
 'TypeError',
 'UnboundLocalError',
 'UnicodeDecodeError',
 'UnicodeEncodeError',
 'UnicodeError',
 'UnicodeTranslateError',
 'UnicodeWarning',
 'UserWarning',
 'ValueError',
 'Warning',
 'WindowsError',
 'ZeroDivisionError',
 '__IPYTHON__',
 '__build_class__',
 '__debug__',
 '__doc__',
 '__import__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'all',
 'any',
 'ascii',
 'bin',
 'bool',
 'bytearray',
 'bytes',
 'callable',
 'chr',
 'classmethod',
 'compile',
 'complex',
 'copyright',
 'credits',
 'delattr',
 'dict',
 'dir',
 'display',
 'divmod',
 'enumerate',
 'eval',
 'exec',
 'filter',
 'float',
 'format',
 'frozenset',
 'get_ipython',
 'getattr',
 'globals',
 'hasattr',
 'hash',
 'help',
 'hex',
 'id',
 'input',
 'int',
 'isinstance',
 'issubclass',
 'iter',
 'len',
 'license',
 'list',
 'locals',
 'map',
 'max',
 'memoryview',
 'min',
 'next',
 'object',
 'oct',
 'open',
 'ord',
 'pow',
 'print',
 'property',
 'range',
 'repr',
 'reversed',
 'round',
 'set',
 'setattr',
 'slice',
 'sorted',
 'staticmethod',
 'str',
 'sum',
 'super',
 'tuple',
 'type',
 'vars',
 'zip']

这个列表中的变量名组成了 Python 中的内置作用域。由于 LEGB 法则,Python 最后将自动搜索这个模块,将会自动得到这个列表中的所有变量名,因此能够使用这些变量名而不需要导入任何模块。因此,有两种方法引用一个内置函数:

1
zip
zip
1
2
import builtins
builtins.zip
zip

重定义内置变量名
本地作用域的变量名可能会覆盖在全局作用域和内置作用域的有着相同变量名的变量,而全局变量名有可能覆盖内置的变量名:

1
2
3
def hider():
open = 'spam'
open('data.txt')
1
hider()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-7-23a713ecf3a4> in <module>()
----> 1 hider()

<ipython-input-5-bd2442a7eccd> in hider()
      1 def hider():
      2     open = 'spam'
----> 3     open('data.txt')

TypeError: 'str' object is not callable

这将隐藏内置的 open 函数,它位于内置的(外部)作用域内,这样 open 将不再在函数内打开文件——它现在是一个字符串,而不是启动函数。如果不需要在此函数中打开文件,那么这不是问题,但是如果试图通过此名称打开文件,则会触发一个错误。不要重新定义需要的内置变量名。

函数也能够简单地使用本地变量名隐藏同名的全局变量:

1
2
3
4
5
6
7
X = 88                # 全局 X

def func():
X = 99 # 局部 X:隐藏了全局 X

func()
print(X)
88

如果在 def 内不增加 global(或nonlocal)声明的话,是没有办法在函数内改变函数外部的变量。

2. global 语句

global 是一个命名空间的声明,它告诉 Python 函数生成一个或多个存在于整个模块内部作用域(命名空间)的全局变量名。

  • 全局变量是位于模块文件内部的顶层的变量名。
  • 全局变量如果是在函数内被赋值的话,必须经过声明。
  • 全局变量名在函数的内部不经过声明也可以被引用。
1
2
3
4
5
6
7
8
X = 88                  

def func():
global X # 增加 global 声明,以便在 def 之内的 X 能够引用在 def 之外的 X
X = 99

func()
print(X)
99
1
2
3
4
y, z = 1, 2
def all_global():
global x
x = y + z

这里 x, y, z 都是全局变量。x 通过 global 语句使自己明确地映射到了模块的作用域。

尽可能避免使用全局变量,通过传递函数然后返回值来替代。

2.1 其他访问全局变量的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# thismod.py

var = 99

def glob1():
global var # global 声明
var += 1 # 改变 global 变量

def glob2():
var = 0
import thismod # 导入自身
thismod.var += 1

def glob3():
var = 0
import sys # 导入系统表
glob = sys.modules['thismod'] # 获取模块对象(或者使用 __name__)
glob.var += 1

3. 作用域和嵌套函数

LEGB 查找法则中的 E 这一层包括了任意嵌套函数内部的本地作用域。

3.1 嵌套作用域的细节

对于一个函数:

  • 一个引用(X)首先在本地(函数内)作用域查找变量名 X;之后会在代码的语法上嵌套了的函数中的本地作用域,从内到外查找;之后查找当前的全局作用域(模块文件);最后再内置作用域内(模块 builtins)。global 声明将会直接从全局(模块文件)作用域进行搜索。
  • 默认情况下,一个赋值(X = value)创建或改变了变量名 X 的当前作用域。如果 X 在函数内部声明为 global,赋值将会创建或改变变量名 X 为整个模块的作用域。另一方面,如果 X 在函数内声明为 nonlocal,赋值会修改最近的嵌套函数的本地作用域中的变量名 X。

3.2 嵌套作用域举例

1
2
3
4
5
6
7
8
X = 99                                 # Global 作用域名:未使用

def f1():
X = 88 # def 局部作用域
def f2():
print(X) # 嵌套函数创建的引用
f2()
f1() # 打印 88:
88

嵌套的 def 在函数 f1 调用时运行;这个 def 生成了一个函数,并将其赋值给变量名 f2,f2 是 f1 的本地作用域内的一个本地变量。在此情况下,f2 是一个临时函数,仅在 f1 内部执行的过程中存在。

当打印变量 x 时,x 引用了存在于函数 f1 整个本地作用域内的变量 x 的值。因为函数能够在整个 def 声明内获取变量名,通过 LEGB 查找法则,f2 内的 x 自动映射到了 f1 的 x。

嵌套作用域查找在嵌套的函数已经返回后也是有效的。

1
2
3
4
5
6
7
8
9
10
X = 99

def f1():
X = 88
def f2():
print(X) # 在嵌套作用域中记住了 X
return f2 # 返回 f2 但不调用它

action = f1() # 创建,返回函数
action() # 现在调用:打印 88
88

命名为 f2 的函数的调用动作的运行是在 f1 运行后发生的。f2 记住了在 f1 中嵌套作用域中的 x,尽管 f1 已经不处于激活状态。

3.3 工厂函数:闭包

函数对象记住嵌套作用域中的值,而不管这些作用域是否仍然存在于内存中。实际上,它们已经附加了内存包(即状态保留),这些内存包在创建的嵌套函数的每个副本中都是本地的,并且通常为类提供一个简单的替代方案。

一个简单的工厂函数
工厂函数(又名闭包)有时用于需要及时生成事件处理、实时对不同情况进行反馈的程序中。例如,假设有一个 GUI,它必须根据用户输入定义操作,而用户输入在构建 GUI 时是无法预料的。在这种情况下,我们需要一个创建并返回另一个函数的函数,该函数的信息可能因所创建的每个函数而有所不同。

1
2
3
4
def maker(N):
def action(X): # 创建并返回函数
return X ** N # action 保留了嵌套作用域的 N
return action

这定义了一个外部的函数,这个函数简单地生成并返回了一个嵌套的函数,却并不调用这个内嵌的函数。如果我们调用外部的函数:

1
2
f = maker(2)                                # 将 2 传递至 N
f
<function __main__.maker.<locals>.action(X)>

得到的是生成的内嵌函数的一个引用。这个内嵌函数是通过运行内嵌的 def 而创建的。如果现在调用从外部得到的那个函数:

1
f(3)                                         # 将 3 传递给 X,N 为 2: 3 ** 2
9
1
f(4)
16

它将会调用内嵌的函数,即 maker 函数内部的名为 action 的函数。在本地作用域内的 N 被作为执行的状态信息保留了下来,这就是为什么返回其参数的平方运算。

如果再调用外层的函数,将得到一个新的有不同状态信息的嵌套函数——得到了参数的三次方而不是平方,但是最初的仍像往常一样是平方。

1
2
g = maker(3)                                   # g 记住了 3,f 记住了 2
g(3) # 3 ** 3
27
1
f(3)                                            # 3 ** 2
9

嵌套的作用域常常被 lambda 函数创建表达式使用,因为它们是表达式,几乎总是嵌套在一个 def 中:

1
2
3
4
5
def maker(N):
return lambda X: X ** N

h = maker(3) # 4 ** 3
h(4)
64

闭包和类
对一些人来说,类在状态保持上似乎更好,因为它们使它们的记忆更加明确地带有属性赋值。类还直接支持闭包函数不支持的其他工具,比如继承定制和操作符重载、更自然地以方法的形式实现多个行为。由于这些区别,类可能更擅长实现更完整的对象。

尽管如此,当保持状态是唯一的目标时,闭包函数通常提供一个更轻便和可行的选择。它们为单个嵌套函数所需的数据提供单独调用的本地化存储。当加上 nonlocal 语句时,这一点尤其正确。nonlocal 语句允许嵌套作用域的状态更改。

从更广泛的角度来看,Python 函数有多种方法在调用之间保留状态。虽然当函数返回时,正常局部变量的值会消失,但可以从调用全局变量中保留值;类实例中的属性;嵌套作用域引用;参数默认值和函数属性。

4. nonlocal 语句

使用 nonlocal 语句,嵌套的 def 可以对嵌套函数中的名称进行读取和写入访问。

nonlocal 应用于一个嵌套的函数的作用域中的一个变量名,而不是所有 def 之外的全局模块作用域。在声明 nonlocal 的时候,它必须已经存在于该嵌套函数的作用域中——它们可能只存在于一个嵌套的函数中,并且不能由一个嵌套的 def 中的第一次赋值创建。

换句话说,nonlocal 即允许对嵌套的函数作用域中的名称赋值,并且把这样的名称的作用域查找限制在嵌套的 def。

4.1 nonlocal 基础

nonlocal 语句只在一个函数内有意义:

1
2
def func():
nonlocal name1, name2, ...

在一条 nonlocal 语句中声明嵌套的作用域名称,使得嵌套的函数能够赋值,并且由此也能够修改这些名称。

nonlocal 使得对该语句中列出的名称的查找从嵌套的 def 的作用域中开始,而不是从声明函数的本地作用域开始。

当执行到 nonlocal 语句的时候,nonlocal 中列出的名称必须在一个嵌套的 def 中提前定义过,否则会产生一个错误。nonlocal 名称只能出现在嵌套的 def 中,而不能在模块的全局作用域中或 def 之外的内置作用域中。

4.2 nonlocal 应用

1
2
3
4
5
6
7
8
def tester(start):
state = start
def nested(label):
print(label, state)
return nested

F = tester(0)
F('spam')
spam 0

tester 构建并返回函数 nested 以便随后调用。

使用 nonlocal 进行修改
在 nested 中把 tester 作用域中的 state 声明为一个 nonlocal,就可以在 nested 函数中修改它了。即便通过名称 F 调用返回的 nested 函数时,tester 已经返回并退出了,这也是有效的:

1
2
3
4
5
6
7
8
9
10
def tester(start):
state = start
def nested(label):
nonlocal state # 在嵌套作用域中记住 state
print(label, state)
state += 1 # 如果是 nonlocal 则允许改变
return nested

F = tester(0)
F('spam')
spam 0
1
F('ham')                                      # 每次调用增加 state
ham 1
1
F('eggs')
eggs 2

通常使用嵌套作用域引用时,我们可以多次调用 tester 工厂函数,以便在内存中获得其状态的多个副本。

1
2
G = tester(42)                                 # 创建一个新的 tester,从 42 开始
G('spam')
spam 42
1
G('eggs')                                      # state 信息更新至 43
eggs 43
1
F('bacon')
bacon 3

边界情况
执行 nonlocal 语句时,nonlocal 名称必须已经在一个嵌套的 def 作用域中赋值过。

1
2
3
4
5
6
def tester(start):
def nested(label):
nonlocal state # nonlocal 必须已经在嵌套的 def 中赋值过
state = 0
print(label, state)
return nested
  File "<ipython-input-16-6579998c1c97>", line 3
    nonlocal state
    ^
SyntaxError: no binding for nonlocal 'state' found
1
2
3
4
5
6
7
8
9
def tester(start):
def nested(label):
global state # 声明 global 时,变量可以之前不存在
state = 0 # 现在在模块中创建了名称
print(label, state)
return nested

F = tester(0)
F('abc')
abc 0
1
state
0

nonlocal 限制作用域查找仅为嵌套的 def,nonlocal 不会在嵌套的模块的全局作用域或所有 def 之外的内置作用域中查找。

1
2
3
4
5
6
7
spam = 99
def tester():
def nested():
nonlocal spam # 必须位于 def 中,而不是模块中
print('Current=', spam)
spam += 1
return nested
  File "<ipython-input-19-79a9b3bb4384>", line 4
    nonlocal spam
    ^
SyntaxError: no binding for nonlocal 'spam' found

5. 为什么使用 nonlocal ?状态保持选项

nonlocal 语句允许在内存中保持可变状态的多个副本,并且解决了在类无法保证的情况下的简单的状态保持。

nonlocal 只能在 Python 3.X 中工作,也有一些替代方法:使用 global 与全局共享状态;使用带属性的类;使用函数属性的状态。

1