最近在Python的学习道路上遇到了几个“拦路虎”:作用域(Scope)、闭包(Closure) 和 装饰器(Decorator)。
刚开始看的时候,什么L、E、G、B作用域链,还有内外函数嵌套,简直让人头大。但经过一番死磕和调试代码后,终于搞懂了它们的运行机制。
为了防止自己以后忘记,也为了帮同样在挣扎的小伙伴理清思路,我决定把这部分知识总结下来。老规矩,文章开头先带大家回顾一下上一节学的函数基础,然后正式进入今天的进阶内容。
📚 第一部分:温故知新(上节函数基础回顾)
在上一阶段的学习中,我们掌握了函数的基本语法:
- 定义与调用:使用
def关键字定义,函数名是变量,指向函数对象。 - 参数传递:位置参数、默认参数、以及
*args和**kwargs处理不定长参数。 - 返回值:使用
return返回结果,不写默认返回None。
掌握了这些,我们就能写出结构清晰的代码了。但如果我们想在函数内部修改全局变量,或者想给函数“动态”增加功能而不改源码,就需要今天学的进阶知识了。
🚀 第二部分:函数进阶核心(本节重点)
今天的内容主要包含三个核心部分:变量作用域、闭包 和 装饰器。这三者是层层递进的关系。
1. 变量的作用域 (LEGB规则)
在Python中,变量不是在任何地方都能被访问的。Python遵循 LEGB 规则来查找变量:
- L (Local):局部作用域,函数内部定义的变量。
- E (Enclosing):闭包函数外的函数中(即外函数的局部作用域)。
- G (Global):全局作用域,模块级别定义的变量。
- B (Built-in):内建作用域,Python自带的内置函数(如
print,len)。
重点笔记:
- 分支和循环没有作用域:在
if或for里定义的变量,在外面是可以访问的。 - 函数才有作用域:函数内部定义的变量(局部变量),函数执行完就会被释放,外部无法访问。
如何在函数内修改全局变量?
这时候需要两个“神器”关键字:
global:在函数内部声明变量为全局变量。nonlocal:在嵌套函数中,修改外层函数(非全局)的变量。
# global 示例
num = 10 # 全局变量
def test():
global num # 声明我要修改全局的num
num = 20
test()
print(num) # 输出 20
# nonlocal 示例 (用于闭包)
def outer():
x = 10
def inner():
nonlocal x # 声明修改的是外层函数的x
x += 1
print(x)
return inner
fn = outer()
fn() # 输出 11
2. 闭包 (Closure)
闭包听起来很高级,其实很简单。如果在一个函数内部定义了另一个函数,并且外部函数返回了内部函数,这就构成了闭包。
构成闭包的3个条件:
- 函数嵌套函数。
- 内部函数引用了外部函数的变量。
- 外部函数返回内部函数。
闭包的作用: 闭包可以保存当前的运行环境(状态)。即使外部函数执行完了,内部函数依然可以使用外部函数的变量。
def outer(b):
a = 10
def inner(): # 内函数
print(a + b) # 引用了外部变量
return inner # 返回内函数
# 这就是一个闭包
closure_func = outer(5)
closure_func() # 输出 15
3. 装饰器 (Decorator)(了解)
装饰器是Python中最经典的应用,它基于闭包实现。核心目的:在不修改原函数代码的前提下,给原函数增加新功能。
应用场景: 比如你想统计每个函数的运行时间,或者检查用户登录权限,就可以用装饰器。
原理: 把原函数作为参数传入装饰器,然后在装饰器里包装一下(先干点别的,再执行原函数,最后再干点别的),最后返回这个包装后的函数。
代码演示(标准写法):
# 定义一个装饰器
def my_decorator(fn):
def eat(*args, **kwargs): # 通用写法,适配任何参数
print("先吃个饭")
result = fn(*args, **kwargs) # 执行原函数
print("再睡个觉")
return result
return eat
# 使用装饰器 (语法糖)
@my_decorator
def hit():
print("打个豆豆")
# 调用
hit()
代码运行结果如图:

注意: @my_decorator 等价于 hit = my_decorator(hit)。
💻 第三部分:课后作业与实战
我整理了几个典型的练习题,用来巩固上面的知识:
1. 实现一个支持两种调用方式的求和函数
要求:my_sum(2, 3) 和 my_sum(2)(3) 都能输出 5。
这道题考察了对参数数量的判断和闭包的使用。
def my_sum(*args):
"""
智能求和函数:支持两种调用方式
方式1: my_sum(a, b) -> 直接传入两个参数
方式2: my_sum(a)(b) -> 柯里化调用,先传入一个参数,返回一个函数,再传入第二个参数
参数:
*args: 可变参数,用于接收任意数量的参数(实际逻辑只处理1个或2个)
返回:
int/float: 两个数字的和
异常:
TypeError: 当参数类型不是数字,或者参数数量错误时抛出
"""
# --- 情况一:参数数量为 1 (需要实现 my_sum(a)(b) 的逻辑) ---
if len(args) == 1:
first_num = args[0]
# 类型校验:确保第一个参数是数字(int或float)
if not isinstance(first_num, (int, float)):
raise TypeError("参数必须是数字类型")
# 定义内部函数 (Inner Function)
# 这里利用了闭包的特性:inner函数可以记住外部函数的变量 first_num
def inner(second_num):
# 类型校验:确保传入的第二个参数也是数字
if not isinstance(second_num, (int, float)):
raise TypeError("参数必须是数字类型")
# 返回两个数的和
return first_num + second_num
# 关键点:这里不执行 inner(),而是返回 inner 函数对象
# 当用户调用 my_sum(2)(3) 时,实际上是先拿到了这个 inner 函数,然后立刻传入 3
return inner
# --- 情况二:参数数量为 2 (需要实现 my_sum(a, b) 的逻辑) ---
elif len(args) == 2:
# 使用 all() 函数和生成器表达式,一次性检查所有参数是否为数字
if not all(isinstance(x, (int, float)) for x in args):
raise TypeError("所有参数必须是数字类型")
# 直接返回两个参数的和
return args[0] + args[1]
# --- 情况三:参数数量错误 ---
else:
# 抛出异常,提示用户正确的参数数量
# 利用 f-string 格式化输出,告诉用户实际收到了几个参数
raise TypeError(f"my_sum 需要 1 个或 2 个参数,但收到了 {len(args)} 个")
# --- 测试代码 ---
# 测试场景 1:标准的两个参数调用
n = my_sum(2, 3)
print(f"my_sum(2, 3) = {n}") # 输出: 5
# 测试场景 2:柯里化调用 (闭包的应用)
m = my_sum(5)(10)
print(f"my_sum(5)(10) = {m}") # 输出: 15
# 测试场景 3:错误的参数类型 (取消注释可看报错效果)
# p = my_sum("a", 3) # 输出: TypeError: 所有参数必须是数字类型
代码运行结果如图:

取消场景三的注释,即报错,显示所有参数必须是数字类型。

2. 随机生成十六进制颜色
考察 random 模块和字符串拼接。
import random
def random_color():
"""
生成随机的十六进制颜色代码
Returns:
str: 以#开头的 7 位十六进制颜色字符串,例如 #FFFFFF、#0033CC
"""
colors = '0123456789ABCDEF'
color_code = '#' + ''.join([random.choice(colors) for _ in range(6)])
return color_code
a = random_color()
print(a)
代码运行结果如图:

注意:每次运行结果都不一样。
3. 给下面的set_age函数添加一个装饰器,
要求:在传入age之前判断age不能小于0,如果小于0则传入0,并打印"提示:年龄不能小于0"
考察装饰器用法。
def check_age(func):
def wrapper(age):
# 核心判断:如果年龄小于0
if age < 0:
print("提示:年龄不能小于0")
# 注意:这里没有调用 func(age),所以原函数的 print 不会执行
# 函数直接在这里结束
else:
# 只有年龄合法时,才执行原函数
func(age)
return wrapper
@check_age
def set_age(age):
print(f'大家好!我今年{age}岁')
# 测试
set_age(20) # 正常情况
set_age(-5) # 异常情况
代码运行结果如图:

🎓 总结
写完这篇博客,感觉自己对Python的理解又深了一层。
- 作用域 是Python查找变量的规则,记住 LEGB 和
global/nonlocal的用法,就能解决大部分变量报错问题。 - 闭包 是函数式编程的思想,它让函数有了“记忆”,能保存状态。
- 装饰器 是Python的“语法糖”,它利用了闭包的特性,让我们可以优雅地在不修改源码的情况下增强函数功能,这在以后会非常常见。
虽然这些概念刚开始有点绕,但只要多写几遍代码,调试一下,就能豁然开朗了!希望这篇笔记能帮到正在学习Python进阶的你。
如果觉得有用,别忘了点赞收藏哦!有什么问题欢迎在评论区交流。

966

被折叠的 条评论
为什么被折叠?



