首页 > 技术文章 > Python名称空间作用域及函数对象与闭包

JZjuechen 2021-11-16 16:42 原文

一、名称空间

名称空间即存放名字与对象映射/绑定关系的地方。

对于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 

名称空间的加载顺序是:内置名称空间—>全局名称空间—>局部名称空间,而查找一个名字,必须从三个名称空间之一找到,查找顺序为:局部名称空间—>全局名称空间—>内置名称空间。

img

二、作用域

2.1 全局作用域与局部作用域

按照名字作用范围的不同可以将三个名称空间划分为两个区域:

  1. 全局作用域:位于全局名称空间、内置名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
  2. 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。

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,若是一直到最外层函数都找不到,则会抛出异常。

img

三、函数对象

函数对象指的是函数可以被当做'数据'来处理,可以分为四个方面的使用,具体如下:

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

img

四、闭包函数

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()
……

闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在下一篇的装饰器中也将大有用处。

img

推荐阅读