前一章我们刚刚讲完装饰器。很多人学装饰器时,会觉得有点神奇:
为什么一个函数能接收另一个函数 为什么函数里面还能再定义函数 为什么外层函数执行完了,里面那个包装函数居然还能继续用到外层的变量
如果你看到这里开始有点发愣,别慌。因为装饰器背后其实正好牵扯到这一章的核心内容:
作用域 闭包
这两个词一听就容易让人紧张。尤其是闭包,很多人第一次听,会本能觉得这是不是特别底层、特别高级、特别难。其实只要你把前面学过的函数、变量、嵌套函数这些东西重新串一下,它就没有那么吓人。
这一章我们就把这件事讲透。你会发现:
闭包不是什么遥远概念,它本质上就是“函数记住了它定义时周围的变量环境”。
而作用域,则是在回答另一个特别关键的问题:
一个变量,到底能在什么地方被访问。
这两件事一旦想明白,前面的装饰器会一下更通,后面的很多高级写法也会顺很多。
一、先从最基础的问题开始:变量到底在哪儿能用
你前面其实早就接触过作用域了,只是那时候还没有系统地叫这个名字。
比如:
name = '张三'defshow_name(): print(name)show_name()
这段代码能正常运行。 因为函数内部能访问外部的 name。
但如果你反过来这样写:
deftest(): age = 18print(age)
这就不行。 因为 age 是在函数内部定义的,它只在函数里面有效,函数外面拿不到。
你会发现,这里其实已经涉及作用域了。
所以先给一个很朴素的定义:
作用域,就是变量能被访问的范围。
这句话一定要先立住。 因为后面所有关于闭包、global、nonlocal 的问题,最后都绕回它。
二、先分清两种最基础的作用域
在你当前阶段,先把最常见的两种分清就够了:
全局作用域 局部作用域
全局作用域,就是在函数外定义的变量。 局部作用域,就是在函数内部定义的变量。
比如:
city = '北京'deftest(): score = 95 print(city) print(score)test()
这里:
city 是全局变量score 是局部变量
函数内部为什么能打印 city
因为函数内部可以访问外层的全局变量。
那函数外部为什么不能打印 score
因为 score 属于局部变量,只在 test() 里面有效。
这就是最基础的作用域关系。
三、为什么作用域特别重要
因为它决定了你写代码时,变量到底是谁的。
比如一个变量是:
只给某个函数自己用 还是整个文件都能用 还是某个嵌套函数和外层函数共用
这些都不是小问题。
如果作用域意识不清,你写代码时就很容易出现这些情况:
变量名冲突 函数里莫名拿不到变量 修改变量时改错层级 明明看起来有变量,结果一运行就报错
所以作用域不是概念装饰品,而是变量管理规则本身。
四、先看一个最常见的坑:函数里为什么有时候能读外部变量,有时候又会报错
比如下面这个例子:
count = 10deftest(): print(count)test()
没问题,会输出:
10
但如果你写成这样:
count = 10deftest(): count = count + 1 print(count)test()
很多人会直觉觉得,这不是把全局的 count 加 1 吗。 其实这通常会报错。
为什么?
因为 Python 一看到你在函数内部给 count 赋值,就会认为:
你这里的 count 是局部变量。
于是这句:
count = count + 1
右边想先读取局部变量 count但这个局部变量此时还没赋值 所以就出问题了
这就是作用域里一个特别经典的坑。
也说明了一件事:
读取变量和修改变量,涉及的作用域判断并不完全一样。
五、这时候就要认识一个重要关键字:global
如果你真的想在函数内部修改全局变量,就要显式告诉 Python:
我这里用的不是新的局部变量 而是外面的全局变量
这时候就用:
global
例如:
count = 10deftest():global count count = count + 1 print(count)test()print(count)
输出:
1111
这里的意思很明确:
函数里的 count,不是新建局部变量 而是去操作全局的那个 count
所以 global 的作用你可以先记成:
在函数内部声明:我要修改全局变量。
不过这里要提前提醒你:
能用,不代表应该乱用。 如果一个程序到处靠 global 改全局状态,后面很容易变乱。
所以当前阶段你要知道它是什么、为什么需要它,但不要养成“有问题就上 global”的习惯。
六、现在开始进入嵌套函数,这才是闭包真正要出现的地方
前面讲的还只是全局和局部。 但闭包真正精彩的地方,往往发生在函数套函数的时候。
比如:
defouter(): x = 10definner(): print(x) inner()outer()
输出:
10
这里就很有意思了。
x 不是全局变量。 它是 outer() 的局部变量。 按理说,局部变量不是只能在自己函数里用吗?
但这里 inner() 居然也能访问到它。
这说明什么?
说明作用域不只是“全局”和“当前函数局部”这么简单。 在嵌套函数里,内层函数还可以访问外层函数的变量。
这就是理解闭包前,必须先建立起来的一层作用域关系。
七、现在可以引出一个非常经典的作用域顺序感了
你现在不用死背术语,但要先有一个顺序感:
当函数里使用一个变量时,Python 会先在当前函数内部找。 如果当前找不到,就往外层函数找。 再找不到,就去全局找。 还找不到,才会报错。
你可以先把这个理解成:
变量会一层一层往外找。
看这个例子:
name = '全局名字'defouter(): name = '外层名字'definner(): name = '内层名字' print(name) inner()outer()
输出:
内层名字
因为 inner() 先在自己这一层就找到了 name。
如果改成这样:
name = '全局名字'defouter(): name = '外层名字'definner(): print(name) inner()outer()
输出:
外层名字
因为 inner() 自己没有 name,就去外层 outer() 找,找到了。
如果再改成:
name = '全局名字'defouter():definner(): print(name) inner()outer()
输出:
全局名字
因为内层和外层都没有,就继续往全局找。
这套一层层往外找的感觉,一定要有。
八、那闭包到底是什么
现在终于可以给定义了。
先给最直白的版本:
闭包,就是内层函数记住并使用了外层函数变量的一种现象或结构。
这句话第一次看可能还是有点书面。我们把它翻译成人话。
闭包的核心味道是:
外层函数里有个变量 内层函数用到了它 而且外层函数执行完以后,这个变量居然还没消失 内层函数还能继续记住并使用它
这就叫闭包。
你可以先把它理解成:
函数把它出生环境里的某些变量,一起“带走了”。
这就是为什么闭包会让很多人一开始觉得有点神奇。 因为它打破了很多人对“函数执行完局部变量就没了”的直觉印象。
九、看一个最经典的闭包例子
defouter(): x = 10definner(): print(x)return innerf = outer()f()
输出:
10
这个例子特别关键,你一定要看懂。
执行 outer() 时,定义了变量 x = 10,然后定义了 inner(),最后返回 inner。
注意,返回的不是 inner() 的执行结果, 而是函数 inner 本身。
也就是说:
f = outer()
执行完以后,f 里拿到的是那个内层函数。
然后再调用:
f()
居然还能打印出 10。
为什么?
因为 inner 这个函数把外层的 x 记住了。 即使 outer() 已经执行完了,这个 x 依然被保留下来,供 inner() 使用。
这,就是闭包最标准、最核心的例子。
十、这件事为什么会让很多人第一次觉得神奇
因为按普通直觉你会觉得:
outer() 都执行完了 它里面的局部变量 x 不是应该没了吗 为什么后面 f() 还能拿到它
这正是闭包的关键价值:
只要内层函数还在引用外层变量,这个变量就不会按普通局部变量那样立刻消失。
你可以把它想成:
原本 x 应该随着 outer() 结束而退场 但因为 inner() 还在用它 所以 Python 把它保留下来了
这就像一个函数把它需要的外部资料打包带走了。 以后即使离开了原来的环境,它还是能继续用这些资料。
这个“记住环境”的能力,就是闭包最迷人的地方。
十一、闭包最常见的实际用途之一:做一个“带状态的函数”
看这个例子:
defmake_counter(): count = 0defcounter(): print(count)return counter
这个版本虽然能访问 count,但还不能修改。 如果想让它真正像计数器,就会碰到一个问题:
内层函数如果想修改外层函数变量,不能直接像全局变量那样用 global。 因为这不是全局变量,而是“外层函数变量”。
这时候,就要认识另一个关键字:
nonlocal
十二、nonlocal 是干什么的
你可以先记一句最实用的话:
nonlocal 用来在内层函数中声明:我要修改外层函数的变量。
注意,它不是改全局变量。 它是改“最近那层外部函数里的变量”。
比如:
defmake_counter(): count = 0defcounter():nonlocal count count += 1 print(count)return counterc = make_counter()c()c()c()
输出:
123
这里就特别有感觉了。
第一次调用 c(),输出 1。 第二次调用,输出 2。 第三次调用,输出 3。
这说明什么?
说明那个外层变量 count 没有随着第一次调用后就重置掉。 它被保存在闭包环境里了。
这就是闭包特别典型的实际价值:
它可以让函数带着自己的状态活下去。
十三、global 和 nonlocal 的区别一定要分清
这两个词很容易混。
global是在函数内部声明:我要修改全局变量
nonlocal是在内层函数内部声明:我要修改外层函数的变量
看两个例子对比一下。
全局变量修改:
x = 10deftest():global x x += 1
外层函数变量修改:
defouter(): x = 10definner():nonlocal x x += 1
你可以这样记:
全局那一层,用 global嵌套函数往外那一层,用 nonlocal
只要这个层级感清楚,就不容易混。
十四、为什么闭包和装饰器关系特别深
因为装饰器底层几乎天然就在用闭包。
比如我们前一章写过:
defdecorator(func):defwrapper(): print('函数开始执行') func()return wrapper
这里的 wrapper() 为什么能在后面继续调用 func?
因为 wrapper 把外层 decorator(func) 里的 func 记住了。
也就是说,wrapper 本身就是一个闭包。 它在外层函数已经执行完后,依然能使用外层传进来的 func。
所以如果你前一章学装饰器觉得还有一点朦胧,这一章应该会突然通很多:
装饰器之所以能工作,很大程度上正是因为闭包让包装函数记住了原函数。
这就是为什么目录里这两章会紧挨着。
十五、再看一个特别接近装饰器本质的例子
defouter(msg):definner(): print(msg)return innerf1 = outer('你好')f2 = outer('欢迎学习 Python')f1()f2()
输出:
你好欢迎学习 Python
这里特别有意思。
f1 和 f2 都是 inner 这种结构出来的函数。 但它们各自记住了不同的 msg。
这就说明,闭包不是简单“共享一个外层变量”, 而是每次创建时,都可以记住当时那份独立环境。
你会发现,这已经特别像工厂模式了:
给我一个参数 我返回一个“带着这个参数记忆”的函数
这就是闭包很强大的地方。
十六、闭包为什么特别适合做“定制函数”
因为它能把参数和行为打包在一起。
比如你想做一个“加法器工厂”:
defmake_adder(n):defadder(x):return x + nreturn adderadd_5 = make_adder(5)add_10 = make_adder(10)print(add_5(3))print(add_10(3))
输出:
813
这里你会发现:
add_5 永远记住了 n = 5add_10 永远记住了 n = 10
这其实已经不是简单函数调用了, 而是在“造函数”。
这种“定制函数”的能力,在很多进阶写法里都会出现。
十七、为什么说闭包是函数高级能力的体现
因为到了闭包这里,函数已经不只是“接收参数、返回结果”那么简单了。
它开始拥有下面这些高级特征:
函数可以作为返回值返回 函数可以记住创建时的环境 函数可以带着状态继续活着 函数可以被工厂化地批量生成 函数可以在不暴露内部变量的情况下持续工作
这说明函数在 Python 里并不只是一个“代码片段”, 而是一个真正的一等对象,并且具备很强的表达力。
所以这章的价值,不只是学会闭包本身, 更重要的是你会开始真正体会:
函数在 Python 里,比很多初学者一开始理解得要强大得多。
十八、一个很常见的误区:把闭包当成必须刻意追求的高级技巧
其实不是。
闭包当然重要, 但它不是那种“我写啥都得硬套进去”的东西。
真正成熟的理解应该是:
当你需要一个函数记住某些外部状态 或者需要用函数工厂定制行为 或者你在读装饰器、回调、框架代码时看到它 你要能认出来,能理解,能适度使用
这就够了。
而不是为了显得懂高级语法,平时什么小逻辑都包成闭包。 那反而会让代码变复杂。
所以闭包要学透,但不要滥用。
十九、本章小练习
你可以做三个特别适合巩固的练习。
练习 1 写一个外层函数,接收一个字符串参数,返回一个内层函数。 内层函数调用时打印这个字符串。
练习 2 写一个 make_multiplier(n),返回一个函数。 这个返回的函数接收一个数字 x,返回 x * n。
练习 3 写一个简单计数器,用 nonlocal 让每次调用都加 1 并打印结果。
参考思路如下:
defouter(msg):definner(): print(msg)return innerf = outer('你好')f()defmake_multiplier(n):defmultiplier(x):return x * nreturn multipliertimes_3 = make_multiplier(3)print(times_3(5))defmake_counter(): count = 0defcounter():nonlocal count count += 1 print(count)return counterc = make_counter()c()c()c()
只要你把这几个例子亲手敲一遍,这一章的主干就会非常稳。
二十、本章总结
这一章最重要的,是把作用域和闭包真正串起来理解。
作用域,就是变量能被访问的范围。 最基础的有全局作用域和局部作用域,而嵌套函数还会引出更细的外层函数作用域。 函数内部读取变量时,会一层层往外找。global 用来在函数内部声明要修改全局变量。nonlocal 用来在内层函数中声明要修改外层函数变量。 闭包的核心,是内层函数记住并使用了外层函数的变量环境。 这也是为什么外层函数执行完以后,内层函数仍然能继续访问那些变量。 装饰器之所以成立,很大程度上正是因为闭包让包装函数记住了原函数。 闭包最典型的价值,是做带状态的函数、函数工厂和定制行为。
下一章我们继续往前走,把这一整个阶段真正收束起来:100|高级语法总结:从“会写”走向“写得漂亮”。