从例子讲起

  看看和自己预测的结果是否一致,知晓对作用域的了解度

e.g.2:

  

 1 b = 6
 2 def f2(a):
 3     print(a)
 4     print(b)
 5     b = 9
 6 
 7 f2(3)
 8 
 9 """
10 输出:
11 3
12 ........
13 ........    print(b)
14 UnboundLocalError: local variable 'b' referenced before assignment
15 """

 

 

e.g.3:

a = 1
b = 2

def f1():
    print(a)
    print(b)


def f2():
    a += 1
    b += 1
f1()
f2()

 

解析e.g.1:

  Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了(第5行:b =9)。

    生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。(稍后解释)

   后面调用 f2(3) 时, f2 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。(在函数体中给b=9进行了赋值操作,在复制操作前尝试打印b)

延伸:

  这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。这比 JavaScript 的行为好多了,avaScript 也不要求声明变量,

    但是如果忘记把变量声明为局变量(使用 var),可能会在不知情的情况下获取全局变量。

 

  字节码验证:

  f1函数的字节码(运行字节码的 CPython VM 是栈机器,因此 LOAD 和 POP 操作引用的是栈):

    dis 模块为反汇编 Python 函数字节码提供了简单的方式

e.g.1:  

def f1(a):
    print(a)
    print(b)
f1(3)#NameError: name 'b' is not defined    3

此处的结果显而易见,b没有定义

from dis import dis


def f1(a):
    print(a)
    print(b)

dis(f1)

 

  8           0 LOAD_GLOBAL              0 (print)           @1
              2 LOAD_FAST                0 (a)               @2
              4 CALL_FUNCTION            1
              6 POP_TOP

  9           8 LOAD_GLOBAL              0 (print)
             10 LOAD_GLOBAL              1 (b)               @3
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE
        

  

  

@1 加载全局名称 print。
@2加载本地名称 a。
@3加载全局名称 b。

  f2函数的字节码

  8           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP

  9           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                1 (b)               @b
             12 CALL_FUNCTION            1
             14 POP_TOP

 10          16 LOAD_CONST               1 (9)
             18 STORE_FAST               1 (b)             
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE
    

  

@b加载本地名称b

这表明,编译器把 b 视作局部变量,即使在后面才为 b 赋值,因为变量的种类(是不是局部变量)不能改变函数的定义体。

为什么在例子e.g.3中f1中可以输出a,b;但是在f2中却不能输出a,b :

  因为f1中函数体中的a,b是自由变量(free variable):因为在函数体中没有对其赋值操作( a += 1; a = a+1),只是调用了a,b的值。

    而f2中的值对a,b进行了赋值操作,隐式的创建了局部变量a,b,此时创建的局部变量并没有事先被定义:声明全局变量关键字global

                                               声明自由变量关键字:nonlocal(在嵌套函数中使用)

Python中的LENGB规则:

  L(local):函数内的变量

  E(engclosing function locals):嵌套作用域

  N(nonlocal) :只作用于嵌套作用域,而且只是作用在函数里面(一般用于闭包)

  G(Global):函数定义所在模块的变量

  B(Builtin):Python内建函数的名字空间

    在Python程序中声明、改变、查找变量名时,都是在一个保存变量名的命名空间中进行中,此命名空间亦称为变量的作用域。

    python的作用域是静态的,在代码中变量名被赋值的位置决定了该变量能被访问的范围。

    即Python变量的作用域由变量所在源代码中的位置决定.变量作用域之LENGB

 

    python引用变量的顺序: 当前作用域局部变量->外层作用域变量->当前模块中的全局变量->python内置变量.
    

    声明全局变量时:先用global关键字声明这个变量,然后再给这个变量赋值 不可直接写成 global  variable  =  1; 

                                          而应 global  x;  variable =1 

e.g.3.modify:

a = 1
b = 2

def f1():
    print(a)
    print(b)


def f2():
    global a, b
    a += 1
    b += 1
    print(a, b)
f1()
f2()
"""
1
2
2 3
"""

 

e.g.2.modify:

 

b = 6
def f2(a):
global b
print(a, end=' ')
print(b, end=' ')
b = 9
f2(3)
print(b, end=' ')
b = 30
f2(3)
print(b, end=' ')
#3 6 9 3 30 9

 

 

 

 

 nonlocal

 用法:这个一般是用在闭包函数里. 但是一定要认识到, nonlocal声明的变量在上级局部作用域内,而不是全局定义, 如果在它声明的变量在上级局部中不存在,则会报错. 实例如下:

e.g.4:

 

def outer():
    x = 100

    def inner():
        x = 200

    inner()
    print(x)

outer()#100

 

 

 那如果要求inner中对x的修改是有效的,必须加上关键字nonlocal

e.g.5:

  nonlocal它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更。(需要理解闭包的概念)

def outer():
    x = 100

    def inner():
        nonlocal x
        x = 200

    inner()
    print(x)

outer()#200

 

e.g.6:

def outer():
    # x = 100   #注释掉 nonlocal上层局部变量

    def inner():
        nonlocal x
        x = 200

    inner()
    print(x)

outer()#SyntaxError: no binding for nonlocal 'x' found

  

 理解nonlocal上层,局部变量:一下代码仍然会报错

x =100
def outer():
    # x = 100

    def inner():
        nonlocal x
        x = 200

    inner()
    print(x)

outer()#SyntaxError: no binding for nonlocal 'x' found

 

 闭包

  闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量

  关键是它能访问定义体之外定义的非全局变量。

看下面的例子:

  计算移动平均值的类:

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)
if __name__ == '__main__':
    avg = Averager()
    print(avg(10))#10.0
    print(avg(11))#10.5
    print(avg(12))#11.0

 

  以上是类的实现,

下面来看一下高阶函数的实现:

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

if __name__ == '__main__':
    avg = make_averager()
    print(avg(10))#10.0
    print(avg(11))#10.5
    print(avg(12))#11.0

 

  注意,这两个示例有共通之处:调用 Averager() 或make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在类中,avg 是 Averager 的实例;

       在高阶函数中avg 是内部函数averager。不管怎样,我们都只需调用 avg(n),把 n放入系列值中,然后重新计算均值。

 

    以类实现的avg实例在哪里存储历史值:很明显:self.series实例属性,那么在高阶函数中,avg对象在哪里寻找series呢?  

  注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10)时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。

  在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量

 

python 变量的作用域与闭包_字节码

 

averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中

的外部变量。

 

 

参考:

Python进阶