Python中的作用域及闭包

Python中的作用域

  在一般编程语言中,作用域从小到大有块级、函数、类、模块、包五个级别。但是在Python中没有块级作用域的存在,比如for语句中的代码的作用域就和for自身所在的作用域同级(if、with等语句中的代码相同)。

  下例在全局作用域中的print函数就可以读取到if语句中的变量a:

if True:
    a = 0

print(a)
# 0

 

Python作用域的分级结构

 Python作用域可分为四个级别:

  • 局部作用域 local
  • 闭包函数外的函数中 enclosing
  • 全局作用域 global
  • 内建作用域 built-in

  变量/函数的查找顺序:l->e->g->b

 当本层没有找到对应的变量/函数后就会去自身的上一层去查找,如果直到内建作用域中依然没有找到就会出现变量/函数没有被定义的错误。

 

a = 0  # global
def outer():
    a = 1  # enclosing
    def inner():
        a = 2  # local
        print(a)
    return inner()

outer()
# 2
a = 0  # global
def outer():
    a = 1  # enclosing
    def inner():
        print(a)
    return inner()

outer()
# 1
a = 0  # global
def outer():
    def inner():
        print(a)
    return inner()

outer()
# 0

  在本层中可以访问上一层或更上层的变量,而不能访问下层或更下层的变量。

  那么我们能不能在本层中去修改上一层变量的值呢?

 

a = 0
def func():
    a = 1
    print(a)

func()
# 1

  如果直接赋值那么func会创建自己的局部变量a,而不是去更改全局变量中的a,我们再看两例:

a = 0
def func():
    b = a
    print(b)

func()
# 0
a = 0
def func():
    a = a+1
    print(a)

func()
# local variable 'a' referenced before assignment
# 原因:‘=’让python认为是在创建局部变量a,所以a+1中的a没有被定义

  从上面的例子中我们可以发现在本层中虽然可以获取上层变量的值,但是不能去修改上层变量的值(‘=’会创建新的局部变量),上层变量对于下层而言是只读的。

  如果需要打破这种限制就要用到global和nonlocal两个关键字。

 

global和nonlocal

  • global:声明变量为全局变量
  • nonlocal:可以在闭包函数的内函数中修改外函数中的变量

 

  我们将函数func中的a通过global声明为全局变量,这样我们就可以在func中修改全局变量a的值了:

a = 0
def func():
    global a
    a = a+1
    print(a)

func()
# 1

  同样的我们可以利用nonlocal在闭包函数的内部函数中去修改外部函数中变量的值:

def outer():
    a = 0
    def inner():
        nonlocal a
        a = a+1
        print(a)
    return inner()

outer()
# 1

 

 在Python中还有可以一次性获取所有全局、局部变量的方法:

  • globals( ) :以字典的方式存储所有全局变量
  • locals ( ):以字典的方式存储所有局部变量

  

闭包

  通过上述知识点,我们就可以引出闭包,先给出闭包的概念:

  在一个外函数中定义了一个内函数,内函数里使用了外函数的临时变量,并且外函数的返回值是对内函数的引用,这样就构成了一个闭包。

  闭包的特殊性在于:一般情况下当一个函数结束,函数内的临时变量都会被释放掉。但是闭包是一种特殊情况,如果外函数在结束的时候发现自身的一个临时变量将来会在内部函数中用到,就会先把这个临时变量绑定给内部函数,然后再释放自身。这样的临时变量叫做内部函数的环境变量。如果函数具有环境变量那么无论在何处被调用都将使用自身的环境变量,而不会受到调用环境中的变量影响。

  简而言之:闭包 = 函数 + 环境变量,环境变量用于记录函数运行时的环境(与函数绑定,不会销毁)

  注意:如果在函数中需要修改其环境变量,则需使用上文提到的nonlocal关键字

 

  让我们举例说明:

 1 a = 0
 2 def outer():
 3     a = 1
 4     def inner():
 5         print(a)
 6     return inner
 7 
 8 f = outer()
 9 f()
10 # 1

  在第8行中,我们通过调用outer函数将inner函数返回给了f,outer中的a变量作为一个临时变量此时应该被释放;在第9行中当我们通过f来调用inner时应该找到全局变量中的a=0并输出0。

  然而实际结果却是输出了1,根据闭包的定义我们可以推得其内部过程:当outer函数被释放时发现自身的临时变量a将来会在内部函数inner中用到,所以将自身的临时变量a绑定给了inner作为其环境变量,于是当使用f来调用inner时输出了1。

  

  案例:假设有一个旅行者,我们需要设计一个run函数,每次调用run函数时旅行者所走的路程+1并且输出当前所走过的总路程。

step = 0
def run():
    global step
    step += 1
    print(step)

run()
run()
run()
# 1 2 3

  使用全局变量自然可以解决这个问题,但是在项目中使用全局变量是一种不好的方式,而通过将路程设置为run函数的一个环境变量就能很好的解决这个问题:

def wrap():
    step = 0
    def run():
        nonlocal step
        step = step+1
        print(step)
    return run

run = wrap()
run()
run()
run()
# 1 2 3

  环境变量step可以不在wrap中定义,而是通过参数传入wrap中:

def wrap(step):
    def run():
        nonlocal step
        step = step+1
        print(step)
    return run

run = wrap(0)
run()
run()
run()
# 1 2 3

 

最后:从另一个角度来说,闭包的存在也让从函数外部得到函数内部的局部变量成为了可能:

def outer():
    a = 0
    def inner():
        return a
    return inner

f = outer()
print(f())
# 0  从outer函数外部得到了outer函数内部的局部变量a

 

posted @ 2020-08-14 20:13  叶迩  阅读(794)  评论(1编辑  收藏  举报