声明:本文参考的文章较多,在头部仅填写一个链接,在具体内容中,将参考来源及链接贴出。


文章目录

  • 1.变量的作用域
  • 1.1 高级语言对数据类型的使用过程
  • 1.2 作用域的产生
  • 1.3 作用域的类型
  • 1.3.1 L-local局部作用域
  • 1.3.2 E-enclosing嵌套作用域
  • 1.3.3 G-global全局作用域
  • 1.3.4 B-Built-in内置作用域
  • 1.4 变量名LEGB法则
  • 2.实例说明
  • 2.1 对变量的引用
  • 2.2 对变量的修改
  • 2.2.1 global关键字
  • 2.2.2 nonlocal关键字
  • 3. 命名空间和作用域的区别
  • 4.Python函数嵌套
  • 5. 闭包


1-3节参考: 作者:范桂飓, 来源:CSDN
原文链接:

1.变量的作用域

在Python程序中创建、改变、查找变量名时,都是在一个保存变量名的空间中进行,我们称之为命名空间,也被称之为作用域。Python的作用域是静态的,在源代码中变量名被赋值的位置决定了该变量能被访问的范围。即Python变量的作用域由变量所在源代码中的位置决定。

1.1 高级语言对数据类型的使用过程

一般的高级语言在使用变量时,都会有下面4个过程。当然在不同的语言中也会有着区别。

  1. 声明变量:让编辑器知道有这一个变量的存在
  2. 定义变量:为不同数据类型的变量分配内存空间
  3. 初始化:赋值,填充分配好的内存空间
  4. 引用:通过引用对象(变量名)来调用内存对象(内存数据)
1.2 作用域的产生

就作用域而言,Python与C有着很大的区别,在Python中并不是所有的语句块中都会产生作用域。只有当变量在Module(模块)、Class(类)、def(函数)中定义的时候,才会有作用域的概念。

def func():
    variable = 100
    print variable
print variable  #输出: NameError: name 'variable' is not defined

在作用域中定义的变量,一般只在作用域中有效。
注意
:在if-elif-else、for-else、while、try-except\try-finally等关键字的语句块中并不会产成作用域。

if True:
    variable = 100
    print variable
    

print "******"
print variable

运行结果:

100     
******
100

变量variable在if语句块内或外都表示同一个变量

1.3 作用域的类型

在Python中,使用一个变量时并不严格要求需要预先声明它,但是在真正使用它之前,它必须被绑定到某个内存对象(被定义、赋值);这种变量名的绑定将在当前作用域中引入新的变量,同时屏蔽外层作用域中的同名变量。

1.3.1 L-local局部作用域

局部变量:包含在def关键字定义的语句块中,即在函数中定义的变量。每当函数被调用时都会创建一个新的局部作用域。Python中也有递归,即自己调用自己,每次调用都会创建一个新的局部命名空间。在函数内部的变量声明,除非特别的声明为全局变量,否则均默认为局部变量。有些情况需要在函数内部定义全局变量,这时可以使用global关键字来声明变量的作用域为全局。局部变量域就像一个 栈,仅仅是暂时的存在,依赖创建该局部作用域的函数是否处于活动的状态。所以,一般建议尽量少定义全局变量,因为全局变量在模块文件运行的过程中会一直存在,占用内存空间。
注意:如果需要在函数内部对全局变量赋值,需要在函数内部通过global语句声明该变量为全局变量。

1.3.2 E-enclosing嵌套作用域

E也包含在def关键字中,E和L是相对的,E相对于更上层的函数而言也是L。与L的区别在于,对一个函数而言,L是定义在此函数内部的局部作用域,而E是定义在此函数的上一层父级函数的局部作用域。主要是为了实现Python的闭包,而增加的实现。

1.3.3 G-global全局作用域

即在模块层次中定义的变量,每一个模块都是一个全局作用域。也就是说,在模块文件顶层声明的变量具有全局作用域,从外部开来,模块的全局变量就是一个模块对象的属性。
注意:全局作用域的作用范围仅限于单个模块文件内

1.3.4 B-Built-in内置作用域

系统内固定模块里定义的变量,如预定义在__builtin__ 模块内的变量。

1.4 变量名LEGB法则

搜索变量名的优先级:局部作用域 > 嵌套作用域 > 全局作用域 > 内置作用域
LEGB法则:
当在函数中使用未确定的变量名时,Python会按照优先级依次搜索4个作用域,以此来确定该变量名的意义。首先搜索局部作用域(L),之后是上一层嵌套结构中def或lambda函数的嵌套作用域(E),之后是全局作用域(G),最后是内置作用域(B)。按这个查找原则,在第一处找到的地方停止。如果没有找到,则会出发NameError错误。
一个LEGB的例子:

globalVar = 100           #G

def test_scope():
    enclosingVar = 200    #E
    def func():
        localVar = 300    #L
print __name__            #B
2.实例说明
2.1 对变量的引用

在不同作用域中可以存在相同的变量名,当出现这种情况的时候,对LEGB法则的理解就显得非常重要了,否则就会有只能知其然而不知其所以然的感觉。
实例1:

def func():
    variable = 300
    print variable

variable = 100
func()
print variable

运行结果如下:

300
100

实例2:

def test_scopt():
    variable = 200
    print variable
    def func():
        print variable   # 这里的变量variable在E中绑定了内存对象200,为函数func()引入了一个新的变量
    func()
    
variable = 100
test_scopt()
print variable

运行结果如下:

200
200
100

例子3:

variable = 300
def test_scopt():
    print variable   #variable是test_scopt()的局部变量,但是在打印时并没有绑定内存对象。
    variable = 200

test_scopt()  # UnboundLocalError: local variable 'variable' referenced before assignment
print variable

上面的例子会报出错误,因为在执行程序时的预编译能够在test_scopt()中找到局部变量variable(对variable进行了赋值)。在局部作用域找到了变量名,所以不会升级到嵌套作用域去寻找。但是在使用print语句将变量variable打印时,局部变量variable并有没绑定到一个内存对象(没有定义和初始化,即没有赋值)。本质上还是Python调用变量时遵循的LEGB法则和Python解析器的编译原理,决定了这个错误的发生。所以,在调用一个变量之前,需要为该变量赋值(绑定一个内存对象)。

注意:为什么在这个例子中触发的错误是UnboundLocalError而不是NameError:name ‘variable’ is not defined。因为变量variable不在全局作用域。Python中的模块代码在执行之前,并不会经过预编译,但是模块内的函数体代码在运行前会经过预编译,因此不管变量名的绑定发生在作用域的那个位置,都能被编译器知道。Python虽然是一个静态作用域语言,但变量名查找是动态发生的,直到在程序运行时,才会发现作用域方面的问题。

例子4:

variable = 300
def test_scopt():
    print variable   # 没有在局部作用域找到变量名,会升级到嵌套作用域寻找,并引入一个新的变量到局部作用域(将局部变量variable赋值为300)。
#    variable = 200

test_scopt()
print variable

运行结果如下:

300
300

比较例子3和4能够更好的理解LEGB的过程。

2.2 对变量的修改

一个non-L的变量相对于L而言,默认是只读而不能修改的。如果希望在L中修改定义在non-L的变量,为其绑定一个新的值,Python会认为是在当前的L中引入一个新的变量(即便内外两个变量重名,但却有着不同的意义)。即在当前的L中,如果直接使用non-L中的变量,那么这个变量是只读的,不能被修改,否则会在L中引入一个同名的新变量。这是对上述几个例子的另一种方式的理解。

注意: 而且在L中对新变量的修改不会影响到non-L的。当你希望在L中修改non-L中的变量时,可以使用global、nonlocal关键字。

variable = 100


def test_scopt():
    variable = 200   # 在L中引入一个新变量,覆盖non-L中的变量
    print variable


test_scopt()
print variable

运行结果如下:

200
100
2.2.1 global关键字

希望在L中修改G中的变量。

spam = 99


def tester():

    def nested():
        global spam
        print 'current=', spam
        spam = 200
    return nested


print spam
tester()()
print spam

运行结果如下:

99
current= 99
200

注意:tester()()表示会自动调用函数tester()的返回值,且此返回值必须为可调用类型,即存在__call__方法。返回一个函数,所以也会执行返回的函数体代码。

2.2.2 nonlocal关键字

在L中修改E中的变量。这是Python3.x增加的新特性,在python2.x中还是无法使用。

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

运行结果如下:

20
20
3. 命名空间和作用域的区别
  • 命名空间:是一个包含了该空间中所有变量和变量值的字典,不同的命名空间之间是相互隔离的,所以在不同的命名空间可以创建同名变量,通过句点标识符来调用和区别,EG. JmilkFan.name & fanguiju.name JmilkFan 和 fanguiju 是两个不同的命名空间。Python 内置了两个查询命名空间的字典的内置函数:

① globals()

>>> x= 3
>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'x': 3, '__doc__': None, '__package__': None}

② locals()

>>> locals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'x': 3, '__doc__': None, '__package__': None}

NOTE: 因为这里的两个例子都是在全局环境中调用,globals()和locals()函数的返回值是一致的。这两个函数有下面点需要注意的地方:

  1. 根据调用地方的不同,globals()和locals()函数可被用来返回全局和局部命名空间里的名字。
  2. 如果在函数内部调用locals(),返回的是所有能在该函数里访问的命名。
  3. 如果在函数内部调用globals(),返回的是所有在该函数里能访问的全局名字。
  4. 两个函数的返回类型都是字典。所以能用keys()函数摘取变量名。
  • 作用域:是一个变量能够有效的区域,全局作用域的全局变量在整个模块中有效,局部作用域中的局部变量只在类或函数中有效。创建一个作用域会同时生成一个命名空间,并且作用域包围了其命名空间。作用域是为了实现变量查询的路径,就如上文所述,如何局部作用域中含有于全局作用域同名的变量时,局部作用域会屏蔽掉全局作用域,这是因为变量的查询路径中,局部作用域要先于全局作用域,然后再到相对的命名空间中获取变量的值。

4-5节两部分参考:
作者:文哥的学习日记, 来源: 简书
原文链接:https://www.jianshu.com/p/17a9d8584530

4.Python函数嵌套

理解了Python中变量的作用域,那么Python函数嵌套就非常容易理解了,我们这里需要注意的一点是Python中的函数也可以当作变量来对待。
python是允许创建嵌套函数的,也就是说我们可以在函数内部定义一个函数,这些函数都遵循各自的作用域和生命周期规则。

def outer():  
    x = 1  
    def inner():  
        print x # 1  
    inner() # 2  
  
outer()

相信大家都知道输出是什么了,输出是1,了解了Python变量的作用域就很容易正确判断函数嵌套的输出啦,不过我还是想多啰嗦两句:
1) #1的地方,python寻找名为x的local变量,在inner作用域内的locals中寻找不到,python就在外层作用域中寻找,其外层是outer函数。x是定义在outer作用域范围内的local变量。
2) #2的地方,调用了inner函数。这里需要特别注意:inner也只是一个变量名,是遵循python的变量查找规则的(Python先在outer函数的作用域中寻找名为inner的local变量)

5. 闭包

闭包的原理我们直接通过下面的例子来解释:

def outer():
    x = 1

    def inner():
        print x  # 1
    return inner


foo = outer()
print foo.func_closure  # 2 doctest: +ELLIPSIS
print dir(foo.func_closure)
foo()

运行结果如下:

(<cell at 0x7fe2a3883210: int object at 0x1850158>,)
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
1

在这个例子中,我们可以看到inner函数作为返回值被outer返回,然后存储在foo变量中,我们可以通过foo()来调用它。但是真的可以跑起来吗?让我们来关注一下作用域规则。

python里运行的东西,都按照作用域规则来运行。

  1. x是outer函数里的local变量
  2. 在#1处,inner打印x时,python在inner的locals中寻找x,找不到后再到外层作用域(即outer函数)中寻 找,找到后打印。

看起来一切OK,那么从变量生命周期(lifetime)的角度看,会发生什么呢:

  1. x是outer的local变量,这意味着只有outer运行时,x才存在。那么按照python运行的模式,我们不能在 outer结束后再去调用inner。
  2. 在我们调用inner的时候,x应该已经不存在了。应该发生一个运行时错误或者其他错误。
    但是这一些都没有发生,inner函数依旧正常执行,打印了x。

Python支持一种特性叫做函数闭包(function closures):在非全局(global)作用域中定义inner函数(即嵌套函数)时,会记录下它的嵌套函数namespaces(嵌套函数作用域的locals),可以称作:定义时状态,可以通过func_closure 这个属性来获得inner函数的外层嵌套函数的namespaces。(如上例中#2,打印了func_closure ,里面保存了一个int对象,这个int对象就是x)

**注意:**每次调用outer函数时,inner函数都是新定义的。上面的例子中,x是固定的,所以每次调用inner函数的结果都一样。

如果上面的x不固定呢?我们继续来看下面的例子:

def outer(x):  
    def inner():  
        print x # 1  
    return inner  
print1 = outer(1)  
print2 = outer(2)  
print print1.func_closure  
print1()  
print print2.func_closure  
print2()

运行结果如下:

(<cell at 0x7fa3207be210: int object at 0x15ad158>,)
1
(<cell at 0x7fa3207be248: int object at 0x15ad140>,)
2

在这个例子中,我们能看到闭包实际上是记录了外层嵌套函数作用域中的local变量。通过这个例子,我们可以创建多个自定义函数。