0%

19、函数作用域.md

一、概念

有了函数之后,我们必须要面对一个作用域的问题。比如:你现在访问一个变量,那么 python 解析器是怎么查找到这个变量,并读取到这个变量的值的呢? 依靠的就是作用域规则!

二、命名空间(namespace)

1、定义

A namespace is a mapping from names to objects

命名空间是名字和对象的映射。也就是可以把一个namespace理解为一个字典,实际上很多当前的Python实现namespace就是用的字典。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

命名空间表示变量的可见范围,一个变量名可以定义在多个不同的命名空间,相互之间并不冲突,但同一个命名空间中不能有两个相同的变量名。比如:两个叫“张三”的学生可以同时存在于班级A和班级B中,如果两个张三都是一个班级,那么带来的麻烦复杂很多了,在Python中你不能这么干。

2、命名空间的种类(2.2以前三种,2.2以后四种)

  1. 局部(local)

    函数内的命名空间就是局部的(函数内部声明但没有使用global的变量)

  2. 全局(global)

    模块内(.py文件内)的命名空间就是全局的

  3. 内置(built-in)

    包括异常类型、内建函数和特殊方法,可以代码中任意地方调用

  4. 闭包(enclosing)

    嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的(闭包函数外的函数中 )

3、作用范围结构图

4、举个栗子

  • 局部

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #test.py
    def fun():
    """
    局部作用域
    """
    name = '吃鸡'
    print(name)
    if __name__ == '__main__':
    fun()
    print(name)#程序出错
  • 全局

    1
    2
    3
    4
    5
    #demo1.py
    txt = '大吉大利,今晚吃鸡'
    print(txt)
    #demo2.py
    print(txt)#程序出错
  • 系统内置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #vars()查看内置全局变量
    """
    全局作用域2
    """
    print(vars)
    if __name__ == '__main__':
    print(__doc__)
    print(__file__)
    print(__package__)
  • 闭包

    1
    2
    3
    4
    5
    6
    7
    def outer():
    num = 200 # 闭包作用域
    def inner():
    num = 300 # 本地作用域
    return num
    return inner
    print outer()() # 输出300

5、举个栗子2

  • 栗子1

    1
    2
    3
    4
    5
    6
    num = 0
    def fun():
    num += 1
    print(num)
    if __name__ == '__main__':
    fun()

    代码分析

    运行结果

    1、通过控制台的错误提示,本地变量“num”在使用之前未被赋值

    2、num + 1中的num是个本地变量, 本地变量,本地变量。python解释器在函数运行时,num = 相当于声明了一个本地变量num ,而num+1解释器先会从本地作用域中查找,所以虽然你在全局中定义了,通过上面的知识不能理解

    3、所以在使用变量前一定要赋值

  • 栗子2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    name = '小明'
    def ex_global():
    # 这里是新创建了一个局部变量name .并不是修改的全局作用域的变量name
    name = '小花'
    print(name)

    if __name__ == '__main__':
    ex_global()
    print(name)
    #输出结果 小花 小明

    代码分析

    1、运行程序在全局作用域中声明了一个name内存中的值是’小明’

    2、执行函数ex_global()在本地作用域声明又声明了一个name的变量值是’小花’

    3、调用函数print(),根据上面说的作用域的查找顺序可以知道先从本地作用域查找,所以第一次输出结果是小花

    4、此时方法执行完毕,执行第二个print(name)由于前面学过的作用域可以知道第二个name变量只会在全局作用域中查找,所以输出小明

  1. 总结
    • 在局部命名空间处,全局命名空间的同名变量是不可见的(只有变量不同名的情况下,可使用 global关键字让其可见)
    • 闭包函数外的函数中
    • 全局命名空间和局部命名空间中, 如果有同名变量,在全局命名空间处,局部命名空间内的同名变量是不可见的
    • 内置命名空间在代码所有位置都是可见的,所以可以随时被调用

三、作用域(scope)

1、说明

A scope is a textual region of a Python program where a namespace is directly accessible.

作用域是Python程序(文本)的某一段或某些段,在这些地方,某个命名空间中的名字可以被直接引用。

简单说就是一个变量的命名空间。代码中变量被赋值的位置,就决定了哪些范围的对象可以访问这个变量,这个范围就是命名空间。python赋值时生成了变量名,当然作用域也包括在内。

简单的来说就是命名空间的可见性

2、查找顺序

  1. 查找顺序结构图

  2. 说明

    • 如果在函数内调用一个变量,先在函数内(局部命名空间)查找,如果找到则停止查找。否则在函数外部(全局命名空间)查找,如果还是没找到,则查找内置命名空间。如果以上三个命名都未找到,则抛出NameError 的异常错误。
    • 如果在函数外调用一个变量,则在函数外查找(全局命名空间,局部命名空间此时不可见),如果找到则停止查找,否则到内置命名空间中查找。如果两者都找不到,则抛出异常。只有当局部命名空间内,使用global 关键字声明了一个变量时,查找顺序则是上面的查找顺序

3、其它

1、块级作用域(语法块)
  • 想想此时运行下面的程序会有输出吗?执行会成功吗?

    1
    2
    3
    4
    5
    6
    7
    8
    #块级作用域 
    if 1 == 1:
    name = "hehe"
    print(name)

    for i in range(10):
    age = i
    print(age)
  • 说明

    代码执行成功,没有问题;在Java/C#中,执行上面的代码会提示name,age没有定义,而在Python中可以执行成功,这是因为在Python中是没有块级作用域的,代码块里的变量,外部可以调用,所以可运行成功;

2、局部作用域
  • 数是个单独的作用域,Python中没有块级作用域,但是有局部作用域

    1
    2
    3
    def  fun():
    name = "hehe"
    print(name)
  • 说明

    1
    2
    3
    4
    5
    6
    # 代码运行出错
    Traceback (most recent call last):
    File "xxx/xxx/xx.py", line 3, in <module>
    print(name)
    NameError: name 'name' is not defined

四、global

1、说明

global语句用来声明一系列变量,这些变量会引用到当前模块的全局命名空间的变量,在函数体外声明的默认就是global

2、举个栗子

  • 栗子1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    num = 0
    def fun():
    global num
    num = num + 1
    print(num)

    if __name__ == '__main__':
    fun()
    print(num)

    代码分析

    运行结果

    1、通过控制台的错误提示,本地变量“num”在使用之前未被赋值

    2、num + 1中的num是个本地变量, 本地变量,本地变量。python解释器在函数运行时,num = 相当于声明了一个本地变量num ,而num+1解释器先会从本地作用域中查找,所以虽然你在全局中定义了,通过上面的知识不能理解

    3、所以在使用变量前一定要赋值

  • 栗子2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    name = '小明'
    def ex_global():
    # 这里是新创建了一个局部变量name
    # 并不是修改的全局作用域的变量name
    name = '小花'
    print(name)
    if __name__ == '__main__':
    ex_global()
    print(name)
    #输出结果 小花 小明

    代码分析

    1、运行程序在全局作用域中声明了一个name内存中的值是’小明’

    2、执行函数ex_global()在本地作用域声明又声明了一个name的变量值是’小花’

    3、调用函数print(),根据上面说的作用域的查找顺序可以知道先从本地作用域查找,所以第一次输出结果是小花

    4、此时方法执行完毕,执行第二个print(name)由于前面学过的作用域可以知道第二个name变量只会在全局作用域中查找,所以输出小明

五、nonlocal(Python3.2引入的)

1、说明

用来在函数或其他作用域中使用外层(非全局)变量

2、举个栗子

  • 栗子1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def outer():
    num = 1
    def inner():
    # 把 num 绑定到外部函数的局部变量 num 上
    nonlocal num
    num = 2
    print("inner:", num)
    inner()
    print("outer:", num)

    if __name__ == '__main__':
    outer()

    代码分析

    1、outer()函数执行

    2、定义一个局部变量 num 并且赋值1

    3、执行inner函数,把 num 绑定到外部函数的局部变量 num 上

    4、赋值num为2,所以第一个print()函数在控制台输出2,函数执行完毕

    5、执行第二个print()方法,由于上一步可以知道,num=2,所以也是输出2,代码执行完毕

4、综合栗子

  1. 栗子1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    def scope_complex():
    def ex_local():
    # 此函数定义了另外的一个var字符串变量,并且生命周期只在此函数内。
    # 此处的var和外层的var是两个变量,
    var = "本地作用域2"

    def ex_nonlocal():
    nonlocal var #使用外层的var变量
    var = "闭包作用域"

    def ex_global():
    global var
    var = "全局作用域"

    var = "局部作用域1"

    ex_local()
    print(var)

    ex_nonlocal()
    print(var)

    ex_global()
    print(var)

    scope_complex()
    print(var)

    代码分析

    1、执行函数 scope_complex() ,定义局部变量 var = “局部作用域1”

    2、执行函数ex_local() ,定义局部变量var = “本地作用域2” ,作用范围仅仅ex_local函数中

    3、执行函数print(var) ,输出的第一步定义的局部变量,而不是第二步定义的局部变量

    4、执行函数ex_nonlocal(),将var变量绑定在第一步定义的变量上,讲var的值改为”闭包作用域”

    5、执行函数print(var),输出”闭包作用域”

    6、执行函数ex_global(),声明全局变量var赋值”全局作用域”

    7、执行函数print(var),先从本地函数作用域查找,所有输出还是”闭包作用域”,

    8、scope_complex() 执行完成

    9、执行函数print(var),从本地作用域中查找,由于在第七步声明了var,所以输出”全局作用域”

  2. 栗子2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def test():
    def ex_nonlocal():
    def ex_nonlocal2():
    nonlocal var
    var = "闭包变量"
    ex_nonlocal2()
    var = "局部变量"
    ex_nonlocal()
    print(var)
    if __name__ == '__main__':
    test()

2、注意

  1. 使用global关键字修饰的变量之前可以并不存在,
  2. 而使用nonlocal关键字修饰的变量在嵌套作用域中必须已经存在。

六、能改变作用域的

  1. python能够改变变量作用域的代码段是def、class、lamda.
  2. if/elif/else、try/except/finally、for/while 并不能涉及变量作用域的更改,也就是说他们的代码块中的变量,在外部也是可以访问的