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

一、函数返回值

先看几个例子

# 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容