跳转至

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] vs L += [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 的模块作用域机制。