六、变量作用域

标识符的作用域是定义为其声明在程序里的可应用范围,也就是变量的可见性

 python中的作用域分4种情况:

L:local,局部作用域,即函数中定义的变量;
    E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的;
    G:global,全局变量,就是模块级别定义的变量;
    B:built-in,系统固定模块里面的变量,比如int, bytearray等。 搜索变量的优先级顺序依次是:
    作用域局部>外层作用域>当前模块中的全局>python内置作用域,也就是LEGB。

 

作用域产生

 

在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如if、try、for等)是不会引入新的作用域的,如下代码:

if 2>1:
    x = 1
print(x)  # 1

这个是没有问题的,if并没有引入一个新的作用域,x仍处在当前作用域中,后面代码可以使用

def test():
    x = 2
print(x) # NameError: name 'x2' is not defined

 

def、class、lambda是可以引入新作用域的。

 

全局变量

1、在一个模块中最高级别的变量有全局作用域

2、全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数,他们的值都是可以被访问的

 

例子1:

# 定义全局变量
a = 100

def test1():
    print(a)

def test2():
    print(a)

# 调用函数
test1()
test2()

执行结果:

100
100

 

 

局部变量

1、定义在函数内的变量有局部作用域
2、局部变量只时暂时地存在,仅仅只依赖于定义它们的函数现阶段是否处于活动

3、当一个函数调用出现时,其局部变量就进入声明它们的作用域。在那一刻,一个新的局部变量名为那个对象创建了

4、一旦函数完成,框架被释放,变量将会离开作用域

5、如果局部与全局有相同名称的变量,那么函数运行时,局部变量的名称将会把全局变量名称遮盖住

 

x = 4

def foo():
    x  = 10
    print "in foo,x=",x

foo()
print "in main,x=",x

执行结果:

in foo,x= 10
in main,x= 4

 

 

核心笔记:搜索标识符

1、当搜索一个标识符的时候,python 先从局部作用域开始搜索

2、如果在局部作用域内没有找到那个名字,那么就一定会在全局域找到这个变量否则就会被抛出 NameError 异常

3、一个变量的作用域和它寄住的名字空间相关

 

 

global语句

1、因为全局变量的名字能被局部变量给遮盖掉,所以为了明确地引用一个已命名的全局变量,必须使用global语句

2、global 的语法如下 :global var1[, var2[, ... varN]]]

3、如果在函数中修改全局变量,那么就需要使用global进行声明,否则出错

 

#!/usr/bin/env python
#coding:utf-8

x = 4

def foo():
    global x 
    x  = 10
    print "in foo,x=",x

foo()
print "in main,x=",x

执行结果:

in foo,x= 10
in main,x= 10

 

 

例子1:

 

a = 1
def f():
    a += 1
    print a

f()

执行结果:

Traceback (most recent call last):
  File "G:/PycharmProject/fullstack2/week1/test.py", line 9, in <module>
    f()
  File "G:/PycharmProject/fullstack2/week1/test.py", line 6, in f
    a += 1
UnboundLocalError: local variable 'a' referenced before assignment

例子2:

li = [1,]
def f2():
    li.append(1)
    print li

f2()
print li

执行结果:

[1, 1]
[1, 1]

 

总结:

1.在函数中不使用global声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。

2.对于不可变类型的全局变量来说,因其指向的数据不能修改,所以不使用global时无法修改全局变量。

3.对于可变类型的全局变量来说,因其指向的数据可以修改,所以不使用global时也可修改全局变量

 

nonlocal关键字

 

global关键字声明的变量必须在全局作用域上,不能在嵌套作用域上,当要修改嵌套作用域(enclosing作用域,外层非全局作用域)中的变量怎么办呢,这时就需要nonlocal关键字了

 注意:nonlocal是在Python3.0中新增的关键字

 

def outer():
    count = 10
    def inner():
        nonlocal count
        count = 20
        print(count)
    inner()
    print(count)
outer()
#20
#20

 

 

 

 

名字空间

1、任何时候,总有一个到三个活动的作用域(内建、全局和局部)

2、标识符的搜索顺序依次是局部、全局和内建

3、提到名字空间,可以想像是否有这个标识符

4、提到变量作用域,可以想像是否可以“看见”这个标识符

 

在这里我们首先回忆一下python代码运行的时候遇到函数是怎么做的。

从python解释器开始执行之后,就在内存中开辟了一个空间

每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来。

但是当遇到函数定义的时候解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心。

等执行到函数调用的时候,python解释器会再开辟一块内存来存储这个函数里的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中。函数中的变量只能在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。

我们给这个“存放名字与值的关系”的空间起了一个名字——叫做命名空间

代码在运行伊始,创建的存储“变量名与值的关系”的空间叫做全局命名空间,在函数的运行中开辟的临时的空间叫做局部命名空间

 

三种命名空间之间的加载与取值顺序:

加载顺序:内置命名空间(程序运行前加载)->全局命名空间(程序运行中:从上到下加载)->局部命名空间(程序运行中:调用时才加载)

取值:

  在局部调用:局部命名空间->全局命名空间->内置命名空间

 

 

 

 

闭包

 

1、如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是closure ,即闭包

2、定义在外部函数内的但由内部函数引用或者使用的变量被称为自由变量
3、闭包将内部函数自己的代码和作用域以及外部函数的作用结合起来

4、闭包的词法变量不属于全局名字空间域或者局部的,而属于其他的名字空间,带着“流浪"的作用域

5、闭包对于安装计算,隐藏状态,以及在函数对象和作用域中随意地切换是很有用的

6、闭包也是函数,但是他们能携带一些额外的作用域

 

闭包实例
在电子商务平台下,每件商品都有一个计数器说明该商品售出数量,这个计数器就可以通过闭包实现

 

#!/usr/bin/env python
#coding:utf8

def counter(start_at = 0):
    count = [start_at]
    def incr():
        count[0] += 1
        return count[0]
    return incr
a = counter()
print a()
b = counter(10)
print b()
print a()

 

执行结果:

1
11
2

 

或者

#!/usr/bin/env python
#coding:utf8

def counter(start_at=0):
    count = [start_at]
    def incr():
        count[0] += 1
        return count[0]
    
    return incr 

if __name__ == "__main__":
    a = counter()
    print a();print a()
    b = counter(10)
    print b();print b()
    print a()

 

函数说明:

1.在函数体内创建另外一个函数是完全合法的,这种函数叫做内部/内嵌函数,所以incr()就是这个内部函数
2.对在外部作用域(但不是在全局作用域)的变量进行引用:count就是被引用的变量,count在外部作用域counter里面,但是不在全局作用域里,
则这个内部函数incr就是一个闭包

 

执行结果:

1
2
11
12
3

 

判断闭包函数的方法__closure__

 

#输出的__closure__有cell元素 :是闭包函数
def func():
    name = 'test1'
    def inner():
        print(name)
    print(inner.__closure__)
    return inner

f = func()
f()

#输出的__closure__为None :不是闭包函数
name = 'test2'
def func2():
    def inner():
        print(name)
    print(inner.__closure__)
    return inner

f2 = func2()
f2()

 

 使用闭包注意事项

 1.闭包中是不能修改外部作用域的局部变量的

#!/usr/bin/env python
#coding:utf-8


def foo():
    m = 0
    def foo1():
        m = 1
        print m

    print m
    foo1()
    print m

foo()

执行结果:

0
1
0

从执行结果可以看出,虽然在闭包里面也定义了一个变量m,但是其不会改变外部函数中的局部变量m

 

2.以下这段代码是在python中使用闭包时一段经典的错误代码

 

def foo():
    a = 1
    def bar():
        a = a + 1
        return a
    return bar

 

这段程序的本意是要通过在每次调用闭包函数时都对变量a进行递增的操作。但在实际使用时

 

>>> c = foo()  
>>> print c()  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
  File "<stdin>", line 4, in bar  
UnboundLocalError: local variable 'a' referenced before assignment

这是因为在执行代码 c = foo()时,python会导入全部的闭包函数体bar()来分析其的局部变量,python规则指定所有在赋值语句左面的变量都是局部变量,则在闭包bar()中,变量a在赋值符号"="的左面,被python认为是bar()中的局部变量。再接下来执行print c()时,程序运行至a = a + 1时,因为先前已经把a归为bar()中的局部变量,所以python会在bar()中去找在赋值语句右面的a的值,结果找不到,就会报错。解决的方法很简单

 

def foo():
    a = [1]
    def bar():
        a[0] = a[0] + 1
        return a[0]
    return bar

c = foo()
print c()

 

执行结果:

2

 

 

只要将a设定为一个容器就可以了。这样使用起来多少有点不爽,所以在python3以后,在a = a + 1 之前,使用语句nonlocal a就可以了,该语句显式的指定a不是闭包的局部变量。

 

 

3.还有一个容易产生错误的事例也经常被人在介绍python闭包时提起,我一直都没觉得这个错误和闭包有什么太大的关系,但是它倒是的确是在python函数式编程是容易犯的一个错误,我在这里也不妨介绍一下。先看下面这段代码

 

for i in range(3):  
    print i

 

在程序里面经常会出现这类的循环语句,Python的问题就在于,当循环结束以后,循环体中的临时变量i不会销毁,而是继续存在于执行环境中。还有一个python的现象是,python的函数只有在执行时,才会去找函数体里的变量的值。

 

 

flist = []
for i in range(3):
    def foo(x): 
        print x + i
    flist.append(foo)
for f in flist:
    f(2)

 

执行结果:

4
4
4

可能有些人认为这段代码的执行结果应该是2,3,4.但是实际的结果是4,4,4。这是因为当把函数加入flist列表里时,python还没有给i赋值,只有当执行时,再去找i的值是什么,这时在第一个for循环结束以后,i的值是2,所以以上代码的执行结果是4,4,4.

解决方法也很简单,改写一下函数的定义就可以了。

flist = []
for i in range(3):
    def foo(x,y=i): 
        print x + y
    flist.append(foo) 
for f in flist:
    f(2)

执行结果:

2
3
4

 

作用

闭包主要是在函数式开发过程中使用。以下介绍两种闭包主要的用途。

 

用途1,当闭包执行完后,仍然能够保持住当前的运行环境。

比如说,如果你希望函数的每次执行结果,都是基于这个函数上次的运行结果。我以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标。

 

#!/usr/bin/env python
#coding:utf-8

origin = [0, 0]  # 坐标系统原点
legal_x = [0, 50]  # x轴方向的合法坐标
legal_y = [0, 50]  # y轴方向的合法坐标
def create(pos=origin):
    def player(direction,step):
        # 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等
        # 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了。
        new_x = pos[0] + direction[0]*step
        new_y = pos[1] + direction[1]*step
        pos[0] = new_x
        pos[1] = new_y
        #注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过
        return pos
    return player

player = create()  # 创建棋子player,起点为原点
print player([1,0],10)  # 向x轴正方向移动10步
print player([0,1],20)  # 向y轴正方向移动20步
print player([-1,0],10)  # 向x轴负方向移动10步

 

执行结果:

[10, 0]
[10, 20]
[0, 20]

用途2,闭包可以根据外部作用域的局部变量来得到不同的结果,这有点像一种类似配置功能的作用,我们可以修改外部的变量,闭包根据这个变量展现出不同的功能。比如有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行。

 

def make_filter(keep):
    def the_filter(file_name):
        file = open(file_name)
        lines = file.readlines()
        file.close()
        filter_doc = [i for i in lines if keep in i]
        return filter_doc
    return the_filter    # 向x轴负方向移动10步

 

如果我们需要取得文件"result.txt"中含有"pass"关键字的行,则可以这样使用例子程序

 

filter = make_filter("pass")  
    filter_result = filter("result.txt")

 

 

 

以上两种使用场景,用面向对象也是可以很简单的实现的,但是在用Python进行函数式编程时,闭包对数据的持久化以及按配置产生不同的功能,是很有帮助的。

 

 小结

 

(1)变量查找顺序:LEGB,作用域局部>外层作用域>当前模块中的全局>python内置作用域;
 
(2)只有模块、类、及函数才能引入新作用域;
 
(3)对于一个变量,内部作用域先声明就会覆盖外部变量,不声明直接使用,就会使用外部作用域的变量;
 
(4)内部作用域要修改外部作用域变量的值时,全局变量要使用global关键字,嵌套作用域变量要使用
nonlocal关键字。nonlocal是python3新增的关键字,有了这个 关键字,就能完美的实现闭包了。