名称空间,作用域和闭包
名称空间,作用域和闭包是函数式编程的基础,在学习中发现有关这些的讲解不够深入,所以查找了一些资料,整理了一下。
名称空间 namespace
什么是名称空间?先看下官方文档的解释:
A namespace is a mapping from names to objects.Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future.
大概意思是:名称空间是名称(变量名,函数名等)和对象的映射,大多数名称空间是通过字典实现。可以理解为名称空间是用于存放名称和对象映射关系的字典。
Examples of namespaces are: the set of built-in names (containing functions such as
abs()
, and built-in exception names); the global names in a module; and the local names in a function invocation. In a sense the set of attributes of an object also form a namespace.
名称空间的例子有存放内置名称的集合,模块中的全局名称以及函数调用中的本地名称。某种意义上说,一个对象属性的集合也组成一个名称空间。
The important thing to know about namespaces is that there is absolutely no relation between names in different namespaces; for instance, two different modules may both define a function
maximize
without confusion — users of the modules must prefix it with the module name.
最重要的是,不同名称空间中的名称之间没有任何关系,而且不同的模块中可以使用同一个名称而不会有任何冲突——调用多个模块时只要在名称前加上其模块名前缀即可。
Namespaces are created at different moments and have different lifetimes. The namespace containing the built-in names is created when the Python interpreter starts up, and is never deleted. The global namespace for a module is created when the module definition is read in; normally, module namespaces also last until the interpreter quits.
不同的名称空间有不同的生命周期,内置名称在 Python 解释器打开时就会被创建,而且不会被删除,一个模块的全局名称空间在模块定义被读入时创建,一般而言模块的名称空间也会保持到解释器关闭。
The local namespace for a function is created when the function is called, and deleted when the function returns or raises an exception that is not handled within the function. (Actually, forgetting would be a better way to describe what actually happens.)
在函数被调用时会创建该函数的本地名称空间,并且当函数返回或者抛出不在函数内部处理的异常时删除(准确地讲是“忘记”)。
以上总结归纳:
- 名称空间是用来存放名称和对象映射关系的字典
- 名称空间有内置名称空间,全局名称空间和本地(局部)名称空间:
- 内置名称空间,打开 Python 解释器就会被创建且不会被删除
- 一个模块的全局名称空间在模块定义被读入时创建,且一般会持续到 Python 解释器关闭
- 一个函数的本地(局部)名称空间,在函数调用时才会创建,当函数结束时被“忘记”。再次调用函数时会重新创建局部名称空间。
- 不同名称空间中的名称之间没有任何关系
作用域 scope
什么又是作用域呢?
作用域是名称或名称空间的作用范围。局部名称空间的作用域称为局部作用域,全局名称空间的作用域称为全局作用域。
A scope is a textual region of a Python program where a namespace is directly accessible. “Directly accessible” here means that an unqualified reference to a name attempts to find the name in the namespace.“Directly accessible” here means that an unqualified reference to a name attempts to find the name in the namespace.
作用域是一个 Python 程序中可以直接访问某个名称空间的文本区域。这里的“直接访问”意味着对名称的直接引用(原文是非限定引用,限定引用的形式是 obj.attr,非限定就是直接引用)将尝试在名称空间中查找该名称。
关于文本区域,我的理解是代码块文本的区域,作用域的嵌套关系是和代码文本的嵌套关系是一致的。
按照自己的理解画了一个示意图,图中函数 outer,middle,inner 有各自的局部名称空间,每个局部空间的作用域是对应函数的文本范围,比如 outer 函数的局部名称空间的作用域就是 outer 函数的文本范围。
名称空间和作用域仔细看上图,图中 outer 函数的名称空间中有名称 v2,middle 函数的名称空间中也有名称 v2,而 inner 函数同时处于这两个名称空间的作用域内,那么 inner 函数使用的是哪个名称空间的 v2 呢?使用的是 middle 名称空间中的 v2 ,原因是就近原则(或称为 LEGB 原则):
- 首先访问自身的名称空间,即本地名称空间(local namespace)
- 其次访问封闭作用域(enclosing scope)的名称空间,封闭作用域的范围介于全局作用域和本地作用域之间
- 再其次访问全局名称空间(global namespace)
- 最后访问内置名称空间(builtin namespace)
这里需要注意,如果一个名称不是在其自身的名称空间内定义的,那么这个名称是只读的,但并不意味着无法修改值,只是无法修改这个名称与对象的绑定。看例子:
def outer():
v1 = 1
v2 = 1
v3 = []
def inner():
print(v1) # 打印 v1 的值
v2 += 1 # 修改 v2 的值
v3.append(1) # 修改 v3 的值
print(v3)
inner()
outer() # 报错,UnboundLocalError: local variable 'v2' referenced before assignment
上面的代码执行后会报错:变量不可在被赋值前引用。这个报错的原因是在 Python 中定义变量不需要关键字,直接使用赋值操作即可定义一个变量,所以 v2 += 1
被解释为定义变量。
现在把 v2 += 1
注释掉再执行,发现不再会报错,而且打印的结果显示 v3 变量确实被添加了一个新元素。原因是名称空间存放的是名称和对象内存地址的映射关系, inner 函数中的 v3.append(1)
语句尽管修改了 v3 的值,但并未修改 v3 绑定对象的地址。像v1
和 v3
这种变量,在一个代码块中被使用但不是在其中定义,则为自由变量。
If a variable is used in a code block but not defined there, it is a free variable.
如果想在 inner 函数中修改 outer 函数中 v2 变量的值,可以在修改 v2 之前使用 nonlocal
语句:nonlocal v2
,这样 v2 变量也会被解释为自由变量。
nonlocal
语句会使得所列出的名称指向最近的封闭作用域(enclosing scope)中定义的变量。nonlocal 的语句格式如下:
nonlocal identifier ("," identifer)* # 多个名称间用逗号隔开
封闭作用域是介于全局作用域和 nonlocal
语句所在的局部作用域之间的作用域,nonlocal
语句中的名称必须在是未在局部作用域中定义的,且在封闭作用域中定义过的。
def outer():
v1 = 'outer defined'
def middle():
v2 = 'middle defined'
def inner():
nonlocal v1, v2 # v1, v2 在局部作用域未定义,且在封闭作用域内定义
v1 += ', inner modified'
v2 += ', inner modified'
inner()
middle()
outer()
和 nonlocal
语句类似的是 global
语句:
global identifier ("," identifer)* # 多个名称间用逗号隔开
global
语句中列出的 标识符(名称)将被解读为全局名称,global
语句中的名称可以是未定义的。看例子:
def outer():
global v1
v1 = 1
def inner():
global v2
v2 = 2
inner()
outer()
print(v1, v2)
理解了名称空间和作用域后,需要认识下 globals() 和 locals() 函数。
globals()函数
globals()
- 参数:无
- 返回:返回一个表示当前全局符号表的字典。这总是当前模块的字典(在函数或方法中,不是调用它的模块,而是定义它的模块)
使用 globals() 函数可以查看全局名称空间
locals()函数
locals()
- 参数: 无
- 返回:更新并返回表示本地符号表的字典。在函数代码块但不是类代码块中调用 locals() 时也返回自由变量。在模块层面上,globals() 函数和 locals() 函数返回相同的字典
闭包 closure
闭包是函数式编程中的重要概念,维基百科中解释闭包是引用了自由变量的函数。这里所说的自由变量不应该是全局变量,因为使用闭包的一个重要的原因就是避免对全局变量的污染——全局变量可以被所有的函数访问,也就有可能被任意函数修改。
先看一个例子:
li = []
def outer():
li = []
def inner():
li.append(1)
return li
return inner
f = outer()
print(f()) # 打印的结果是 [1]
print(f()) # 打印的结果是 [1, 1]
print(li) # 打印的结果是 []
这个例子实现了闭包,每次调用函数 f 时,都会修改自由变量 li 的值,而全局变量 li 不受影响。这里可以发现 f 函数引用的变量 li 并没有随 outer 函数调用停止而删除,这验证了官方文档中提到的,函数的名称空间会在函数调用后”忘记“:
The local namespace for a function is created when the function is called, and deleted when the function returns or raises an exception that is not handled within the function. (Actually, forgetting would be a better way to describe what actually happens.)
另外下面这个例子可以证明函数每次调用都会创建一个新的名称空间:
li = []
def outer():
li = []
def inner():
li.append(1)
return li
return inner
print(outer()()) # 打印的结果是 [1]
print(outer()()) # 打印的结果是 [1]
print(li) # 打印的结果是 []
这段代码说明,函数 outer 的每次调用都会创建一个新的名称空间,两个名称空间中 li 绑定的不是同一个数组对象,所以两次打印的结果是相同的。
综上所述,Python 中闭包实现的基础是函数每次调用都会创建一个新的名称空间,而且函数执行完毕后名称空间不会被删除。另外需要一个变量绑定闭包函数,通过重复调用这个变量才能实现闭包的功能。
网友评论