美文网首页
Python 函数之二(返回值、作用域、LEGB、销毁)

Python 函数之二(返回值、作用域、LEGB、销毁)

作者: Alexander_Zz | 来源:发表于2021-01-18 21:20 被阅读0次

    一、函数返回值

    先看几个例子

    # return 语句之后可以执行么?
    def showplus(x):
        print(x)
        return x + 1
        print('~~end~~')
    
    showplus(5)
    
    # 多条 return 语句都会执行么?
    def showplus(x):
        print(x)
        return x + 1
        return x + 2
    
    showplus(5)
    
    # 下例多个 return 可执行么?
    def guess(x):
        if x > 3:
            return "> 3"
        else:
            return "<= 3"
    
    print(guess(10))
    
    # 下面函数执行的结果是什么
    def fn(x):
        for i in range(x):
            if i > 3:
                return i
        else:
            print("{} is not greater than 3".format(x))
    
    print(fn(5))   # 打印 4
    print(fn(3))   # 打印 None
    

    总结

    • Python 函数使用 return 语句返回 “返回值”
    • 所有函数都有返回值,若没有 return 语句,隐式调用 return None
    • return 语句并不一定是函数的语句块的最后一条语句
    • 一个函数可存在多个 return 语句,但只有一条可被执行,若没有一条 returm 语句被执行,隐式调用 return None
    • 若有必要,可显示调用 return None,可简写为 return
    • 若函数执行了 return 语句,函数就会返回,当前被执行的 return 语句之后的其他语句就不会被执行了
    • 返回值的作用:结束函数调用、返回 “返回值”

    能够一次返回多个值么?

    def showvalues():
        return 1, 3, 5
    
    showvalues()
    
    • 函数不能同时返回多个值
    • return 1, 3, 5 看似返回多个值,隐式的被 python 封装成了一个 元组
    • x, y, z = showlist() 使用解构提取返回值更为方便

    二、函数 作用域

    2.1 作用域

    一个标识符的可见范围,这就是标识符的作用域,一般常说的是变量的作用域

    def foo():
        x = 100
    
    print(x)   # 可以访问到么?
    

    上例中 x 不可被访问到,会抛出异常 NameError: name 'y' is not defined,原因在于函数是一个封装,它会开辟一个 作用域x 变量被限制在这个作用域中,所以在函数外部 x 变量 不可见

    注意:每一个函数都会开辟一个作用域

    2.2 作用域分类
    • 全局作用域
      • 在整个程序运行环境中都可见
      • 全局作用域中的变量称为全局变量
    • 局部作用域
      • 在函数、类等内部可见
      • 局部作用域中的变量称为局部变量,其使用范围不能超过其所在局部作用域
    # 局部变量
    def fn1():
        x = 1   # 局部作用域,x 为局部变量,使用范围在 fn1 内
        
    def fn2():
        print(x)
        
    print(x)
    
    # 全局变量
    x = 5   # 全局变量,也在函数外定义
    def foo():
        print(x)
    
    foo()
    
    • 一般来讲外部作用域变量在函数内部可见,可以使用
    • 反过来,函数内部的局部变量,不能在函数外部看到
    2.3 函数嵌套

    在一个函数中定义了另一个函数

    def outer():
        def inner():
            print("inner")
        print("outer")
        inner()
    outer()   # 可行么?
    inner()   # 可行么?
    

    内部函数 inner 不能在外部直接使用,会抛 NameError 异常,因为它在函数外部不可见

    其实, inner 不过就是一个标识符,就是一个函数 outer 内部定义的变量而已

    嵌套结构作用域
    对比下面嵌套结构,代码执行的效果

    def outer1():
        o = 65
        def inner():
            print("inner {}".format(o))
            print(chr(o))
            
        inner()
        print("outer {}".format(o))
        
    outer1()
    
    def outer2():
        o = 65
        def inner():
            o = 97
            print("inner {}".format(o))
            print(chr(o))
            
        inner()
        print("outer {}".format(o))
        
    outer2()
    

    从执行的结果来看

    • 外层变量在内部作用域可见
    • 内层作用域 inner 中,若定义了 o = 97,相当于在当前函数 inner 作用域中 重新定义了一个新的变量 o,但 这个 o 并不能覆盖外部作用域 outer2 中的变量 o,只不过对于 inner 函数来说,其只能可见自己的作用域中定义的变量 o
    2.4 一个复制语句的问题

    再看下例


    示例.png

    仔细观察函数 2 返回的错误指向 x += 1,原因是什么呢?

    x = 5
    def foo():
        x += 1
    foo()   # 报错如下
    
    示例.png

    原因分析

    • x += 1 其实是 x = x + 1
    • 相当于在 foo 内部定义了一个局部变量 x,那么 foo 内部所有 x 都是这个局部变量 x
    • x = x + 1 相当于使用了局部变量 x,但是这个 x 还没有完成赋值,就被右边拿来做加 1 操作了

    如何解决这个常见问题?

    2.5 global 语句
    x = 5
    def foo():
        global x   # 全局变量
        x += 1
        print(x)
    foo()
    
    • 使用 global 关键字的变量,将 foo 内的 x 声明为使用 外部的全局作用域 中定义的 x
    • 全局作用域中必须有 x 的定义

    若全局作用域中没有 x 定义会怎样?
    注意,下面实验若在 ipython、jupyter 中做,上下文运行环境中有可能有 x 的定义,稍微不注意,就测试不出效果

    # 有错么?
    def foo():
        global x
        x += 1
        print(x)
    foo()
    
    # 有错么?
    def foo():
        global x
        x = 10
        x += 1
        print(x)
    foo()
    print(x)   # 可以么?
    

    使用 global 关键字定义的变量,虽然在 foo 函数中声明,但是这将告诉当前 foo 函数作用域,这个 x 变量将使用外部全局作用域中的 x

    即使实在 foo 中又写了 x=10,也不会在 foo 这个局部作用域中定义局部变量 x

    使用了 globalfoo 中的 x 不再是局部变量了,它是全局变量

    总结

    • x+=1 这种是特殊形式产生的错误的原因?先应用后赋值,而 Python 动态语言是赋值才算定义,才能被引用。解决办法,在这条语句前增加 x=0 之类的赋值语句,或使用 global 告诉内部作用域,去全局作用域查找变量定义
    • 内部作用域使用 x = 10 之类的赋值语句会重新定义局部作用域使用的变量 x,但是,一旦这个作用域宠使用 global 声明 x 为全局的,那么 x=5 相当于在为全局作用域的变量 x 赋值

    global 使用原则

    • 外部作用域变量会在内部作用域可见,但也不要在这个内部的局部作用域中直接使用,因为函数的目的就是为了封装,尽量与外界隔离
    • 若函数需要使用外部全局变量,请尽量使用函数的形参定义,并在调用传实参解决
    • 一句话:不用 global。学些它就是为了深入理解变量作用域
    2.6 闭包

    自由变量:未在本地作用域中定义的变量,例如定义在内层函数外的外层函数的作用域中的变量
    闭包:就是一个概念,出现在嵌套函数中,至的是 内层函数引用到了外层函数的自由变量,就形成了闭包。

    def counter():
        c = [0]
        def inc():
            c[0] += 1
            return c[0]
        return inc
    foo = counter()
    print(foo(), foo())
    c = 100
    print(foo())
    

    上例代码分析

    • 第七行会执行 counter()返回 inc 对应的函数对象,注意这个函数对象并不释放,因为有 foo 记住
    • 第四行会报错么?
      • 不会报错,因为修改的是 counter() 的本地变量 cindex 为 0 的元素的值,而不是重新定义 c 变量
    • 第八行打印什么结果?
      • 打印 1 2
    • 第十行打印什么结果?
      • 打印 3
      • 第九行的 ccounter 中的 c 不一样,而 inc() 引用的是自由变量 counter() 中的变量 c

    这是 Python 2 中实现闭包的方式,Python 3 还可使用 nonlocal 关键字

    def counter():
        count = 0
        def inc():
            count += 1
            return count
        return inc
    
    foo = counter()
    print(foo(), foo())
    

    上例一定出错,使用 global 可解决

    def counter():
        global count
        count = 0
        def inc():
            global count
            count += 1
            return count
        return inc
    
    foo = counter()
    print(foo(), foo())
    

    上例使用 global 解决,这是全局变量的实现,而不是闭包了
    若对这个普通变量使用闭包,Python 3 中可使用 nonlocal 关键字

    2.7 nonlocal 语句

    nonlocal:将变量标记为不在本地作用域定义,而是在 上级的某一级局部作用域 中定义,但 不能是全局作用域 中定义

    def counter():
        count = 0
        def inc():
            nonlocal count   # 声明变量 count 不是本地变量
            count += 1
            return count
        return inc
    
    foo = counter()
    print(foo(), foo())
    

    count 是外层函数的局部变量,被内部函数引用
    内部函数使用 nonlocal 关键字声明 count 变量在上级作用域而非本地作用域中定义
    代码中内层函数应用外部局部作用域中的自由变量,形成闭包

    示例.png

    上例是错误的,nonlocal 声明变量 a 不在当前作用域,但是往外就是全局作用域了,所以报错

    三、默认值的作用域

    示例.png
    示例.png

    为何第二次调用 foo() 打印的值 [1, 1]

    • 因为函数也是对象,每个函数定义被执行后,就生成一个函数对象和函数名这个标识符关联
    • Python 把函数的默认值放在了函数对象的属性中,这个属性就伴随着这个函数对象的整个生命周期
    • 查看 foo.__defaults__ 属性为元祖
    def foo(xyz=[], m=123, n='abc'):
        xyz.append(1)
        print(xyz)
        
    print(id(foo), foo.__defaults__)
    foo()
    print(id(foo), foo.__defaults__)
    foo()
    print(id(foo), foo.__defaults__)
    

    函数地址并没有变,就是说 foo() 这个函数对象没有变过,调用它,他的属性 __defaults__ 中使用元祖保存默认值 xyz 的默认值为引用类型,引用类型的元素变动,并不是元祖的变化

    # 非引用类型缺省值
    def foo(xyz, m=123, n='abc'):
        m = 456
        n = 'def'
        print(xyz)
        
    print(foo.__defaults__)
    foo('rookie')
    print(foo.__defaults__)
    

    属性 __defaults__ 中使用元祖保存所有位置参数默认值,不会因为在函数体内改变了局部变量(形参)的值而发生改变

    # keyword-only 参数的缺省值
    def foo(xyz, m=123, *, n='abc', t=[1, 2]):
        m = 456
        n = 'def'
        t.append(300)
        print(xyz, m, n, t)
        
    print(foo.__defaults__, foo.__kwdefaults__)
    foo('rookie')
    print(foo.__defaults__, foo.__kwdefaults__)
    

    属性 __defaults__ 中使用元组保存所有位置参数默认值
    属性 __kwdefaults__ 中使用字典保存所有 keyword-only 参数的默认值

    def x(a=[]):
        a += [5]
        
    print(x.__defaults__)
    x()
    x()
    print(x.__defaults__)
    
    def y(a=[]):
        a = a = [5]
        
    print(y.__defaults__)
    y()
    y()
    print(y.__defaults__)
    

    输出结果不一致

    • + 表示两个列表合并并返回一个新列表
    • += 表示就地修改一个列表,在其后追加一个列表。就是 extend 方法

    四、变量名解析原则 LEGB

    • Local,本地作用域、局部作用域的 local 命名空间。函数调用时创建,调用结束消亡
    • Enclosing,Python 2.2 时引入了嵌套函数,实现了闭包,这个就是嵌套函数的外部函数的命名空间
    • Global,全局作用域,即一个模块的命名空间。模块被 import 时创建,解释器退出时消亡
    • Build-in,内置模块的命名空间,生命周期从 Python 解释器启动时创建到解释器退出时消亡。例如 print(open)printopen 都是内置的变量

    所以一个名词的查找顺序就是 LEGB


    示例.png

    五、函数的销毁

    • 定义一个函数就是生成一个函数对象,函数名指向的就是函数对象
    • 可使用 del 语句删除函数,使其引用计数减一
    • 可使用同名标识符覆盖原有定义,本质上也是使其引用计数减一
    • Python 程序结束时,所有对象销毁
    • 函数也是对象,也不例外,是否销毁,还是看引用计数是否减为 0

    相关文章

      网友评论

          本文标题:Python 函数之二(返回值、作用域、LEGB、销毁)

          本文链接:https://www.haomeiwen.com/subject/wgfpaktx.html