Python名称空间作用域及函数对象与闭包
一、名称空间
名称空间即存放名字与对象映射/绑定关系的地方。
对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x表示清除该绑定关系。
在程序执行期间最多会存在三种名称空间:
1.1 内置名称空间
伴随Python解释器的启动/关闭而产生/回收,因而是第一个被加载的名称空间,用来存放一些内置的名字,比如内置函数名:
>>> max
<built-in function max> # built-in:内置
1.2 全局名称空间
伴随Python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下列名字:
模块名sys:
import sys
变量名x:
x = 1
变量名y:
if x == 1:
y = 2
函数名foo:
def foo(x):
y = 1
def bar():
pass
类名Bar:
Class Bar:
pass
1.3 局部名称空间
伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中。
调用函数时,才会执行函数体代码,名字x和y都存放于该函数的局部名称空间中:
def foo(x):
y = 3
名称空间的加载顺序是:内置名称空间—>全局名称空间—>局部名称空间,而查找一个名字,必须从三个名称空间之一找到,查找顺序为:局部名称空间—>全局名称空间—>内置名称空间。
二、作用域
2.1 全局作用域与局部作用域
按照名字作用范围的不同可以将三个名称空间划分为两个区域:
- 全局作用域:位于全局名称空间、内置名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
- 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。
2.2 作用域与名字查找的优先级
在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找:先查找全局名称空间,没有找到,则再查找内置名称空间,最后都没有找到就会抛出异常:
x = 100 # 全局作用域的名字x
def foo():
x = 300 # 局部作用域的名字x
print(x) # 在局部找x
foo() # 结果为300
在全局作用域查找名字时,起始位置便是全局作用域,所以先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常:
在全局找x,结果为100:
x = 100
def foo():
x = 300 # 在函数调用时产生局部作用域的名字x
foo()
print(x)
提示:可以调用内置函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。在全局作用域查看到的locals()的结果等于globals()。
Python支持函数的嵌套定义,在内嵌的函数内查找名字时,会优先查找自己局部作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域:
x = 1
def outer():
x = 2
def inner(): # 函数名inner属于outer这一层作用域的名字
x = 3
print('inner x:%s' % x)
inner()
print('outer x:%s' % x)
outer()
结果为:
inner x:3
outer x:2
在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字:
x = 1
def foo():
global x # 声明x为全局名称空间的名字
x = 2
foo()
print(x) # 结果为2
当实参的值为可变类型时,函数体内对该值的修改将直接反应到原值:
num_list = [1, 2, 3]
def foo(nums):
nums.append(5)
foo(num_list)
print(num_list)
结果为:
[1, 2, 3, 5]
对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局):
def f1():
x = 2
def f2():
nonlocal x
x = 3
f2() # 调用f2(),修改f1作用域中名字x的值
print(x) # 在f1作用域查看x
f1() # 结果为3
nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。
三、函数对象
函数对象指的是函数可以被当做'数据'来处理,可以分为四个方面的使用,具体如下:
3.1 函数可以被引用
>>> def add(x, y):
... return x + y
...
>>> func = add
>>> func(1, 2)
3
3.2 函数可以作为容器类型的元素
>>> dic = {'add':add, 'max':max}
>>> dic
{'add': <function add at 0x100661e18>, 'max': <built-in function max>}
>>> dic['add'](1, 2)
3
3.3 函数可以作为参数传入另外一个函数
>>> def foo(x, y, func):
... return func(x, y)
...
>>> foo(1, 2, add)
3
3.4 函数的返回值可以是一个函数
>>> def bar():
... return add
...
>>> func = bar()
>>> func(1,2)
3
四、闭包函数
4.1 闭与包
基于函数对象的概念,可以将函数返回到任意位置去调用,但作用域的关系是在定义完函数时就已经确定了的,与函数的调用位置无关。
x = 1
def f1():
def f2():
print(x)
return f2
def f3():
x = 3
f2 = f1() # 调用f1()返回函数f2
f2() """需要按照函数定义时的作用关系去执行,与调用位置无关"""
f3() """结果为1"""
也就是说函数被当做数据处理时,始终以自带的作用域为准。若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该‘内嵌函数’就是闭包函数,简称闭包(Closures)
x = 1
def outer():
x = 2
def inner():
print(x)
return inner
func = outer()
func() """结果为2"""
可以通过函数的__closure__属性,查看到闭包函数所包裹的外部变量:
>>> func.__closure__
(<cell at 0x10212af78: int object at 0x10028cca0>,)
>>> func.__closure__[0].cell_contents
2
“闭”代表函数是定义在函数内部的,“包”代表函数外‘包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。
4.2 闭包的用途
目前为止,我们得到了两种为函数体传值的方式,一种是直接将值以参数的形式传入,另一种就是将值包给函数:
方式一:
import requests
def get(url):
return requests.get(url).text
方式二:
import requests
def page(url):
def get():
return requests.get(url).text
return get
提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,需要事先安装:pip3 install requests
对比两种方式,方式一在下载同一页面时需要重复传入url,而方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url:
方式一下载同一页面:
get('https://www.python.org')
get('https://www.python.org')
get('https://www.python.org')
……
方式二下载同一页面:
python = page('https://www.python.org')
python()
python()
python()
……
闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在下一篇的装饰器中也将大有用处。