11.2 函数进阶与类 —— 作用域¶
内容梗概¶
本课在进入"面向对象与类"之前,先讲清 Python 中变量(名称)的作用域问题,并与 C 语言的头文件/全局变量机制做对比。核心内容包括:模块(文件)作用域与函数局部作用域的区分、global 关键字的作用、嵌套函数下的名称搜索顺序(LEGB 思路的直观讲解)、nonlocal 关键字,以及一个极易混淆的特例——函数内对列表参数用 = 赋值与用 += 追加的截然不同行为。老师用画图("连连看")方式逐行推演了一个经典的嵌套作用域示例,期间自己讲错一次后当场纠正,也借此提醒学生这些概念很容易绕晕。
知识点详解¶
引入:Python 的作用域与 C 语言的对比 (约 00:00 - 00:47)¶
- C 语言中在头文件定义变量即为全局变量,影响范围取决于多少文件引用了该头文件。
- Python 中需要弄清:变量/名字的作用域是怎样的、如何查看作用域。
局部变量与模块作用域 (约 00:49 - 04:30)¶
- 例子:函数
demo内部定义test = 1,函数外(文件级)定义test = 2;调用demo()后print(test)输出 2。 - 原因:函数内的
test是局部变量,只在函数内有效;Python 中**一个文件就是一个模块**,文件级(模块)作用域里的test始终是外面那个,函数内的赋值不影响它。 - 变量定义位置(在前在后)不影响结果;赋值语句本身即完成定义(不需要像有些同学习惯的那样先初始化
test = 0)。
global 关键字 (约 03:28 - 04:30)¶
- 若要在函数内访问/修改最外层(模块级)的变量,需先用
global test声明,之后的赋值就作用于全局变量;此后函数内对test的修改会让外面的test变为新值(演示打印出 1)。 - 注意:函数的作用域本身没有变化,只是函数内操作的那个名字被绑定到了全局变量上。
嵌套作用域与名称搜索顺序(经典示例)(约 04:37 - 23:00)¶
以 PPT 第 9 页的经典示例逐行推演(老师在屏幕上画图"连连看"):
def scope_test(): # 外层函数,内有局部 spam
def do_local():
spam = "local spam" # 只影响自己的局部作用域
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam" # 修改外层 scope_test 的 spam
def do_global():
global spam
spam = "global spam" # 修改模块级 spam
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
- 作用域层次(嵌套):模块(module)作用域 →
scope_test函数作用域 → 三个内部函数各自的作用域。模块级和scope_test级各有自己的spam,三个内部函数同级。 - 搜索顺序:访问一个名字时,先在最内部(局部)作用域找;找不到则向上退一级找最近的非局部定义;
global则直接指向**本模块**最外层(不会超出模块)。 - 逐步推演(终极目标是能在大脑中算出每句 print 的输出):
spam = "test spam":定义在scope_test的局部作用域。do_local()内的赋值只定义了自己内部的spam,不影响外层。故第一行打印After local assignment: test spam。do_nonlocal()用nonlocal声明后,内部不定义新名字,直接找到"非局部、离我最近"的spam(即scope_test的spam)并修改为nonlocal spam。第二行打印nonlocal spam。do_global()修改的是**模块级**spam(变为global spam),但scope_test自己的spam没变。第三行打印仍是nonlocal spam。- 函数返回后模块级
print输出In global scope: global spam。 - 最终四行输出:
test spam、nonlocal spam、nonlocal spam、global spam。 - 老师在此处一度讲错(误以为第三行会打印
global spam),当场更正:do_global改了全局变量,但scope_test里 print 时按搜索顺序找到的仍是自己作用域的spam,因为没有对它做任何改变。这也说明该知识点确实容易绕。
特例:= 赋值 vs += 追加(参数为列表时)(约 24:20 - 38:10)¶
"考试题"式互动例子:
- 情形一:
demo(L)内执行L = [1, 2, 3],外面L = [3, 4, 5],调用后打印 L —— 输出 [3, 4, 5]。原因:=在函数局部命名空间**新定义了一个名字叫 L**,让它指向新列表对象[1, 2, 3],与传进来的参数指向脱钩,外面的列表不受影响。 - 情形二:只加一个加号,
L += [1, 2, 3]—— 输出 [3, 4, 5, 1, 2, 3]。 - 原理:
L = [...]是"创建新名字 + 指向新对象";而L += [...]没有定义任何新的局部名称,它相当于调用列表对象自身的方法(等价于L.extend([1,2,3]),也即连续 append)。按搜索顺序,函数内没有局部 L,就找到参数传进来的 L(指向外面的[3,4,5]),于是在**原对象上就地追加**了 1、2、3,外面的 L 随之改变。 - 为什么是 345 在前:
+=是在后面追加(类比字符串a += "123"; a += "456"得到123456)。 - 把
+=改写成L.extend([1,2,3])后对照搜索逻辑就更容易理解:函数内没有局部变量 L,自然访问到参数(外部)的 L。 - 关键结论:
L = X和L += X看似只差一个加号,实质区别是**是否在局部作用域产生了新名字**;+=写法很有迷惑性,对列表这类可变对象尤其要注意。 - 结尾提醒:真实项目中别人写代码一般会尽量清楚、可读,不太会遇到这么绕的情况,但遇到了要有这个意识。
示例与演示¶
- 基础演示:函数内
test = 1、模块级test = 2,调用函数后打印test得 2;加global test后函数内赋值,外面打印变为 1。 - 嵌套作用域推演:PPT 第 9 页
scope_test/do_local/do_nonlocal/do_global示例,老师在屏幕上用不同颜色画出各作用域的spam并连线对应关系,预测四行输出后运行验证。 - 互动"考试题":
demo(L)中L = [1,2,3]vsL += [1,2,3],让学生猜输出(345 还是 123、还是"468"),现场运行验证并解释;用L.extend(...)改写加深理解。
重点与难点¶
- 名称搜索顺序:最内层作用域 → 外层(nonlocal,最近的非局部定义)→ 模块级(global)。看代码时要能在脑中"连连看",预判每个
spam/test指向哪一层的哪个名字。 global只到模块(文件)为止,不会跨文件;nonlocal找的是"非局部且最近"的那一层。- 内部函数用
=赋值会在**自己的局部作用域新建名字**,屏蔽外层同名变量,不影响外层——这是最常见的误区。 - **
= 与 += 的本质区别**(本课最大难点):=定义新名字并指向新对象;+=是调用对象自身方法就地修改(等价extend/多次append`),不产生新局部名字,因而会改动传进来的外部可变对象(列表)。老师自己也在这里讲错过一次,足见其易错。 nonlocal/global声明后,函数内对该名字的赋值都作用于声明所指的那一层。
关联内容¶
- 前置知识:函数定义与调用、参数传递、列表及其方法(
append、extend)、模块的概念(一个 .py 文件就是一个模块,文件/目录相关章节)。 - 与本章其他课的关系:承接 11.1(函数进阶),为 11.3(类)铺垫——类的方法中大量涉及
self、属性与作用域问题,先搞清作用域再学面向对象。 - 与后续的关系:理解可变对象(列表)在函数调用中的就地修改行为,对后面实验课阅读第三方库代码、避免隐蔽 bug 很重要。
- 与 C 语言课程的对比:头文件/全局变量机制 vs Python 的模块作用域机制。