Kodewick
第 18 课,共 31 课目录

作用域

第 18 课,共 31 课 · 第 3 部分 · 函数 · 约 15 分钟

变量的作用域,就是程序里能用这个名字的那一片地方。到第 15 课为止,所有变量都住在同一个大空间里。有了函数就不一样了:每次调用都有自己的一块地方放变量,调用结束,这块地方也就没了。

main.py
1def light_street():2    lamps = 33    print(f"点亮了 {lamps} 盏灯")4 5light_street()6print(lamps)
输出
点亮了 3 盏灯
Traceback (most recent call last):
  File "main.py", line 6, in <module>
    print(lamps)
NameError: name 'lamps' is not defined

发生了什么: Python 不认识「lamps」这个名字。可能是拼错了,或者用它之前还没给它赋值。

局部变量

lamps 是在函数里建的,所以是局部变量:它属于这一次调用。出了函数,这个名字就不存在了,最后一行停在 NameError 上。参数也是局部变量。

小技巧: 上面的例子点单步看看。调用一开始,lamps 出现在函数自己的那组变量里;调用一结束,这组变量就没了,lamps 也跟着没了。

想从函数里拿出一个值,别去够它的局部变量,用 return:

main.py
1def light_street():2    lamps = 33    return lamps4 5count = light_street()6print(count)
输出
3

每次调用都从头开始。局部变量不会记得上一次调用时的值。

全局变量

在所有函数外面、程序最外层建的变量,是全局变量。函数可以读它:

main.py
1price = 42 3def cost(count):4    return count * price5 6print(cost(3))7price = 58print(cost(3))
输出
12
15

函数每次跑的时候才去找 price,所以看到的是最新的值。

同名的两个变量

在函数里面给一个名字赋值,总是建一个新的局部变量,哪怕外面有一个同名的全局变量。全局的那个不会被碰到。

先猜一猜

最后一行显示什么?

1lamps = 102 3def night_shift():4    lamps = 25    print(f"里面:{lamps}")6 7night_shift()8print(f"外面:{lamps}")

改全局变量:global

那函数想给一个全局的计数器加 1,会怎样?Python 在函数跑之前就定好了:函数里有地方给 count 赋值,所以整个函数里的 count 都是局部的。于是 count += 1 要读局部的 count 时,它还没有值,程序停在 UnboundLocalError 上:

main.py
1count = 02 3def add_lamp():4    count += 15 6add_lamp()
输出
Traceback (most recent call last):
  File "main.py", line 6, in <module>
    add_lamp()
  File "main.py", line 4, in add_lamp
    count += 1
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

发生了什么: 程序在这里停下来了。

要改全局变量,就在函数开头用 global 说清楚:

main.py
1count = 02 3def add_lamp():4    global count5    count += 16 7add_lamp()8add_lamp()9print(count)
输出
2

global 能用,但少用。会改全局变量的函数难读,因为随便哪里的一次调用都可能把值改掉。把值传进去、把结果 return 出来,一般更清楚。

嵌套函数和 nonlocal

函数里面还可以定义函数。里面的函数是外面函数的局部变量,它能读外面函数的变量。想改外面的变量,用 nonlocal:意思是「包着我的那个函数里的变量」。

main.py
1def light_route(streets):2    lit = 03    def light_one(street):4        nonlocal lit5        lit += 16        print(f"{lit}. {street}")7    for street in streets:8        light_one(street)9    return lit10 11print(f"一共 {light_route(['磨坊路', '集市'])} 盏")
输出
1. 磨坊路
2. 集市
一共 2 盏

不写 nonlocal 的话,lit += 1 会让 lit 变成 light_one 的局部变量,和上面全局变量的例子一样出错。点单步:light_one 跑的时候,能同时看到它自己的变量和 light_route 的变量。

Python 去哪里找:LEGB

Python 碰到一个名字,会按顺序去四个地方找,用找到的第一个:

字母作用域里面有什么
LLocal,局部当前函数的变量和参数
EEnclosing,外层函数包着它的那些函数的变量
GGlobal,全局程序最外层建的变量
BBuilt-in,内置Python 一直都有的名字:print、len、sum……
main.py
1name = "全局"2 3def outer():4    name = "外层函数"5    def inner():6        print(name)7        print(len(name))8    inner()9 10outer()
输出
外层函数
4

inner 自己没有 name(L),所以 Python 先在 outer 里找到了(E),轮不到全局的那个。len 前三个地方都没有,所以来自内置(B)。四个地方都找不到,就是 NameError。

别挡住内置的名字

内置是最后才找的。所以变量叫 list、sum、max,就把同名的内置函数挡住了,下一次调用它就会出错:

main.py
1list = ["铁", "金"]2print(list)3letters = list("ore")
输出
['铁', '金']
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    letters = list("ore")
TypeError: 'list' object is not callable

发生了什么: 它不是函数,后面不能加括号去调用。

现在 list 是你的列表,列表不能调用。换个名字,比如 ores 或 items。尤其小心 list、dict、str、sum、max、min、input 和 type。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。每道题的起始代码都有一个作用域的问题:先运行,读一读报错。

练习 1

拿出来

最后一行报 NameError。改一改,让函数返回总数,最后一行显示它。

要显示
12
exercise1.py
1def add_up(loads):2    total = sum(loads)3 4add_up([3, 4, 5])5print(total)
输出
点「运行」或按 Ctrl+Enter
练习 2

数一数调用了几次

ring() 每调用一次,就该给全局的 rings 加 1。让它能用。

要显示
3
exercise2.py
1rings = 02 3def ring():4    rings += 15 6ring()7ring()8ring()9print(rings)
输出
点「运行」或按 Ctrl+Enter
练习 3

被挡住的内置函数

变量 sum 挡住了内置的 sum(),所以最后一行出错。给变量换个名字,让两行都能显示。

要显示
10
3
exercise3.py
1loads = [3, 5, 2]2sum = 03for ore in loads:4    sum += ore5print(sum)6print(sum([1, 2]))
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 函数里建的变量是局部的,只在这次调用里存在。想拿出值,用 return。
  • 最外层建的全局变量,函数里可以读。
  • 在函数里给一个名字赋值,它就是局部的,哪怕有同名的全局变量;赋值之前去读,就是 UnboundLocalError。
  • global 名字 让函数能改全局变量;nonlocal 名字 让嵌套函数能改外层函数的变量。
  • Python 按「局部、外层函数、全局、内置」的顺序找名字。别给变量起 list、sum、max 这种名字。