python进阶_关于命名空间与作用域

时间:2022-03-18 22:48:03

写在前面

如非特别说明,下文均基于Python3

命名空间与作用于跟名字的绑定相关性很大,可以结合另一篇介绍Python名字、对象及其绑定的文章。

1 def fun1():
2   i = 1
3  
4 def fun2():
5   i = 2

同一个模块中的两个函数中,两个同名名字i之间绝没有任何关系,因为它们分属于不同明明空间。

1.2 命名空间的种类

常见的命名空间有:

built-in名字集合,包括像abs()这样的函数,以及内置的异常名字等。通常,使用内置这个词表示这个命名空间-内置命名空间

模块全局名字集合,直接定义在模块中的名字,如类,函数,导入的其他模块等。通常,使用全局命名空间表示。

函数调用过程中的名字集合,函数中的参数,函数体定义的名字等,在函数调用时被“激活”,构成了一个命名空间。通常,使用局部命名空间表示。

一个对象的属性集合,也构成了一个命名空间。但通常使用objname.attrname的间接方式访问属性,而不是直接访问,故不将其列入命名空间讨论。

类定义的命名空间,通常解释器进入类定义时,即执行到class ClassName:语句,会新建一个命名空间。(见官方对类定义的说明)

1.3 命名空间的生命周期

不同类型的命名空间有不同的生命周期:

内置命名空间,在Python解释器启动时创建,解释器退出时销毁;

全局命名空间,模块的全局命名空间在模块定义被解释器读入时创建,解释器退出时销毁;

局部命名空间,这里要区分函数以及类定义。函数的局部命名空间,在函数调用时创建,函数返回或者由未捕获的异常时销毁;类定义的命名空间,在解释器读到类定义创建,类定义结束后销毁。(关于类定义的命名空间,在类定义结束后销毁,但其实类对象就是这个命名空间内容的包装,见官方对类定义的说明)

2. 作用域

2.1 什么是作用域

作用域是Python的一块文本区域,这个区域中,命名空间可以被“直接访问”。这里的直接访问指的是试图在命名空间中找到名字的绝对引用(非限定引用)。这里有必要解释下直接引用和间接引用:

直接引用;直接使用名字访问的方式,如name,这种方式尝试在名字空间中搜索名字name。

间接引用;使用形如objname.attrname的方式,即属性引用,这种方式不会在命名空间中搜索名字attrname,而是搜索名字objname,再访问其属性。

2.2 与命名空间的关系

现在,命名空间持有了名字。作用域是Python的一块文本区域,即一块代码区域,需要代码区域引用名字(访问变量),那么必然作用域与命名空间之间就有了联系。

顾名思义,名字作用域就是名字可以影响到的代码文本区域,命名空间的作用域就是这个命名空间可以影响到的代码文本区域。那么也存在这样一个代码文本区域,多个命名空间可以影响到它。
作用域只是文本区域,其定义是静态的;而名字空间却是动态的,只有随着解释器的执行,命名空间才会产生。那么,在静态的作用域中访问动态命名空间中的名字,造成了作用域使用的动态性。

那么,可以这样认为:

静态的作用域,是一个或多个命名空间按照一定规则叠加影响代码区域;运行时动态的作用域,是按照特定层次组合起来的命名空间。

在一定程度上,可以认为动态的作用域就是命名空间。在后面的表述中,我会把动态的作用域与其对应命名空间等同起来。

2.3 名字搜索规则

在程序中引用了一个名字,Python是怎样搜索到这个名字呢?

在程序运行时,至少存在三个命名空间可以被直接访问的作用域:

Local
首先搜索,包含局部名字的最内层(innermost)作用域,如函数/方法/类的内部局部作用域;

Enclosing
根据嵌套层次从内到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封闭函数的作用域。如两个嵌套的函数,内层函数的作用域是局部作用域,外层函数作用域就是内层函数的 Enclosing作用域;

Global
倒数第二次被搜索,包含当前模块全局名字的作用域;

Built-in
最后被搜索,包含内建名字的最外层作用域。

程序运行时,LGB三个作用域是一定存在的,E作用域不一定存在;若程序是这样的:

i = 1
print(i)

局部作用域在哪里呢?我们认为(Python Scopes And Namespaces):

Usually, the local scope references the local names of the (textually) current function. Outside functions, the local scope references the same namespace as the global scope: the module's namespace. Class definitions place yet another namespace in the local scope.

一般地,局部作用域引用函数中定义的名字。函数之外,局部作用域和全局作用域引用同一个命名空间:模块的明星空间。然而类型的局部作用域引用了类定义新的命名空间。

Python按照以上L-E-G-B的顺序依次在四个作用域搜索名字。没有搜索到时,Python抛出NameError异常。

2.4 何时引入作用域我们知道:

我们知道:

在Python中一个名字只有在定义之后,才能引用。

print(i)

直接引用未定义的名字i,按照搜索规则,在LGB三个作用域均没有搜索到名字i(LB相同命名空间)。抛出NameError异常:

Traceback (most recent call last):
 File "scope_test.py", line 15, in <module>
  print(i)
NameError: name 'i' is not defined

那对于这段代码呢?

def try_to_define_name():
  '''函数中定义了名字i,并绑定了一个整数对象1'''
  i = 1
 
try_to_define_name()
print(i) #引用名字i之前,调用了函数

 在引用名字i之前,明明调用了函数,定义了名字i,可是还是找不到这个名字:

1 Traceback (most recent call last):
2  File "scope_test.py", line 20, in <module>
3   print(i) #引用名字i之前,调用了函数
4 NameError: name 'i' is not defined

虽然定义了名字i,但是定义在了函数的局部作用域对应的局部命名空间中,按照LEGB搜索规则,在全局作用域中自然访问不到局部作用域;再者,函数调用结束后,这个命名空间被销毁了。

引用名字总是与作用域相关的,因此:

在Python中一个名字只有在定义之后,才能在合适的作用域引用。

那么,在定义名字时,就要注意名字定义的作用域了,以免定义后需要访问时却找不到。所以,了解Python在何时会引入新的作用域很有必要。一般来说,B,G两个作用域的引入在不能够通过代码操作的,能够通过语句引入的作用域只有E,L了。Python中引入新作用域的语句很有限,总的来说只有两类一个:

函数定义引入local作用域或者Enclosing作用域;本质上,lambda和生成器表达式也是函数,会引入新作用域。

类定义引入local作用域;

列表推导式引入local作用域,传说在python2中列表推导式不引入新的作用域

几个会让有其他高级语言经验的猿困惑的地方:

if语句:

if True:
  i = 1
print(i) # output: 1,而不是NameError

if语句并不会引入新的作用域,所以名字绑定语句i = 1与print(i)是在同一个作用域中。

for语句:

for i in range(6):
  pass
print(i) #output: 5,而不是NameError

for语句同样不会引入新的作用域,所以名字i的绑定和重绑定与print(i)在同一个作用域。这一点Python就比较坑了,因此写代码时切忌for循环名字要与其他名字不重名才行。

import语句:

def import_sys():
  '''import sys module'''
  import sys
 
import_sys()
print(sys.path) # NameError: name 'sys' is not defined

这个算非正常程序员的写法了,import语句在函数import_sys中将名字sys和对应模块绑定,那sys这个名字还是定义在局部作用域,跟上面的例子没有任务区别。要时刻切记Python的名字,对象,这个其他编程语言不一样,但是:

打破第一编程语言认知的第二门编程语言,才是值得去学的好语言。

3. 作用域应用

3.1 *变量可读不可写

我不太想用“变量”这个词形容名字,奈何变量是家喻户晓了,Python中的*变量:

If a variable is used in a code block but not defined there, it is a free variable.

如果引用发生的代码块不是其定义的地方,它就是一个*变量。专业一点,就是:

引用名字的作用域中没有这个名字,那这个名字就是*名字

Note: “*名字”只是作者YY的,并没得到广泛认可。

我们已经了解了作用域有LEGB的层次,并按顺序搜索名字。按照搜索顺序,当低层作用域不存在待搜索名字时,引用高层作用域存在的名字,也就是*名字:

实例一:

def low_scope():
  print(s)
 
s = 'upper scope'
low_scope()

很清楚,这段代码的输出是upper scope。

1 def low_scope():
2   s = 'lower scope'
3  
4 s = 'upper scope'
5 low_scope()
6 print(s)

很遗憾,最后的打印语句没有按照期待打印出lower scope而是打印了upper scope。

Python的一个怪癖是,如果没有使用global语句,对名字的赋值语句通常会影响最内层作用域。
即赋值语句影响局部作用域,赋值语句带来的影响是绑定或重绑定,但是在当前局部作用域的命名空间中,并没有s这个名字,因此赋值语句在局部作用于定义了同名名字s,这与外层作用域中的s并不冲突,因为它们分属不同命名空间。
这样,全局作用域的s没有被重绑定,结果就很好解释了。

当涉及可变对象时,情况又有所不同了:

def low_scope():
  l[0] = 2
 
l = [1, 2]
low_scope()
print(l) # [2, 2]

很遗憾,最后的打印语句没有按照期待输出[1, 2]而是输出了[2, 2]。
上一个例子的经验并不能运用在此,因为list作为一个可变对象,l[0] = 2并不是对名字l的重绑定,而是对l的第一个元素的重绑定,所以没有新的名字被定义。因此在函数中成功更新了全局作用于中l所引用对象的值。

注意,下面的示例跟上面的是不一样的:

1 def low_scope():
2   l = [2, 2]
3  
4 l = [1, 2]
5 low_scope()
6 print(l) # [1, 2]

我们可以用本节中示例1的方法解释它。

综上,可以认为:

*变量可读不可写。

3.2 global和nonlocal

总是存在打破规则的需求:

在低层作用域中需要重绑定高层作用域名字,即通过*名字重绑定。

于是global语句和nonlocal语句因运而生。

global语句是适用于当前代码块的声明语句。列出的标识符被解释为全局名字。虽然*名字可以不被声明为global就能引用全局名字,但是不使用global关键字绑定全局名字是不可能的。

nonlocal语句使得列出的名字指向最近封闭函数中绑定的名字,而不是全局名字。默认的绑定行为会首先搜索局部作用域。nonlocal语句使得在内层函数中重绑定外层函数作用域中的名字成为可能,即使同名的名字存在于全局作用域。

def scope_test():
   
  def do_local():
    spam = 'local spam'
 
  def do_nonlocal():
    nonlocal spam # 当外层作用域不存在spam名字时,nonlocal不能像global那样自作主张定义一个
    spam = 'nonlocal spam' # *名字spam经nonlocal声明后,可以做重绑定操作了,可写的。
 
  def do_global():
    global spam # 即使全局作用域中没有名字spam的定义,这个语句也能在全局作用域定义名字spam
    spam = 'global spam' # 自有变量spam经global声明后,可以做重绑定操作了,可写的。
 
  spam = 'test spam'
  do_local()
  print("After local assignment:", spam) # After local assignment: test spam
  do_nonlocal()
  print("After nonlocal assignment:", spam) # After nonlocal assignment: nonlocal spam
  do_global()
  print("After global assignment:", spam) # After global assignment: nonlocal spam
 
 
scope_test()
print("In global scope:", spam) # In global scope: global spam

作者说不行nonlocal的邪:

def nest_outter():
  spam = 'outer'
 
  def nest_inner():
    nonlocal spam1
    spam1 = 'inner'
 
  nest_inner()
  print(spam)
 
nest_outter()

Output:

File "scope_test.py", line 41
  nonlocal spam1
SyntaxError: no binding for nonlocal 'spam1' found

坑坑坑

4.1 坑1 - UnboundLocalError

def test():
  print(i)
  i = 1
 
i = 2
test()

Output:

Traceback (most recent call last):
 File "scope_test.py", line 42, in <module>
  test()
 File "scope_test.py", line 38, in test
  print(i)
UnboundLocalError: local variable 'i' referenced before assignment

其实忽略掉全局作用域中i = 2这条语句,都可以理解。

Python对局部作用域情有独钟,解释器执行到print(i),i在局部作用域没有。解释器尝试继续执行后面定义了名字i,解释器就认为代码在定义之前就是用了名字,所以抛出了这个异常。如果解释器解释完整个函数都没有找到名字i,那就会沿着搜索链LEGB往上找了,最后找不到抛出NameError异常。

4.2 坑2 - 类的局部作用域

 1 class Test(object):
 2  
 3   i = 1
 4  
 5   def test_print(self):
 6     print(i)
 7  
 8 t = Test()
 9 i = 2
10 t.test_print()

我就问问大家,这个输出什么?
当然会出乎意料输出2了,特别是有其他语言经验的人会更加困惑。

上文强调过:
函数命名空间的生命周期是什么? 调用开始,返回或者异常结束,虽然示例中是调用的方法,但其本质是调用类的函数。
类命名空间的作用域是什么?类定义开始,类完成定义结束。

类定义开始时,创建新的属于类的命名空间,用作局部作用域。类定义完后,命名空间销毁,没有直接方法访问到类中的i了(除非通过间接访问的方式:Test.i)。

方法调用的本质是函数调用:

class Test(object):
 
  i = 1
 
  def test_print(self):
    print(i)
 
t = Test()
i = 2
# t.test_print()
Test.test_print(t) # 方法调用最后转换成函数调用的方式

函数调用开始,其作用域与全局作用域有了上下级关系(L和G),函数中i作为*名字,最后输出2。
因此,不能被类中数据成员和函数成员的位置迷惑,始终切记,Python中两种访问引用的方式:

直接引用:试图直接写名字name引用名字,Python按照搜索LEGB作用域的方式搜索名字。

间接引用:使用objname.attrname的方式引用名字attrname,Python不搜索作用域,直接去对象里找属性。

4.3 坑3 - 列表推导式的局部作用域

1 a = 1
2 b = [a + i for i in range(10)]
3 print(b) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Output:

Traceback (most recent call last):
 File "scope_test.py", line 15, in <module>
  class Test(object):
 File "scope_test.py", line 18, in Test
  b = [a + i for i in range(10)]
 File "scope_test.py", line 18, in <listcomp>
  b = [a + i for i in range(10)]
NameError: name 'a' is not defined

输出反馈名字a未定义。

上文强调过,解释器读取类定义开始class ClassName后,创建命名空间用作局部作用域。
语句a = 1,在这个局部作用域中定义了名字i
语句b = [a + i for i in rage(10)],列表推导式同样创建了一个局部作用域。这个作用域与类定义的局部作用域并没有上下级关系,所以,自然没有任何直接访问名字a的方法。

Python中只有四种作用域:LEGB,因为类定义的局部作用域与列表推导式的局部作用域于不是嵌套函数关系,所以并不能构成Enclosing作用域关系。因此它们是两个独立的局部作用域,不能相互访问。

既然是两个独立局部作用域,那么上述例子就等同于:

1 def test1():
2   i = 1
3  
4 def test2():
5   print(i)
6  
7 test1()
8 test2()

期待在test2中访问test1的名字i,显然是不可行的