闭包 · 作用域 · 装饰器

一、闭包(Closure)

1. 定义

闭包 = 函数 + 它记住的外部变量。

一个函数记住了它定义时所在的环境(外部变量),即使外部函数已经执行完了,它还能用那些变量。

2. 最简单的例子

1
2
3
4
5
6
7
8
9
10
def outer():
message = "你好" # 外部变量

def inner(): # 内部函数
print(message) # 用到了外部变量

return inner # 返回内部函数

f = outer() # outer 已经执行完了
f() # 输出:你好

outer() 执行完后,message 本该消失,但 f() 还能打印出来——因为 inner 把它记住了。

3. 闭包的三个条件

  1. 有嵌套函数(函数里面定义函数)
  2. 内部函数用到了外部函数的变量
  3. 外部函数返回内部函数

4. 计数器例子

1
2
3
4
5
6
7
8
9
10
11
12
def counter():
count = 0
def add():
nonlocal count # 声明要修改外部变量
count += 1
return count
return add

c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3

count 是私有的,外面访问不到。

5. 函数工厂例子

1
2
3
4
5
6
7
8
9
10
def make_multiplier(n):
def multiply(x):
return x * n
return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15

6. 总结

闭包就是:函数把外部的变量”打包”带走了,走到哪用到哪。

常见用途:装饰器、计数器、函数工厂、数据隐藏。


二、作用域:locals() 和 globals()

1. 通俗理解

  • locals():查看当前房间里有什么(局部作用域)
  • globals():查看整栋大楼里有什么(全局作用域)

它们都返回一个字典。

2. locals() 例子

1
2
3
4
5
6
7
def my_room():
name = "小明"
age = 18
print(locals())

my_room()
# 输出:{'name': '小明', 'age': 18}

在函数外面调用 locals(),其实就等于 globals()。

3. globals() 例子

1
2
3
4
5
6
7
8
9
10
score = 99  # 全局变量

def check():
local_var = "我是局部的"
print("全局变量有:", globals()['score'])
print("全局里没有 local_var:", 'local_var' in globals())

check()
# 全局变量有: 99
# 全局里没有 local_var: False

4. 对比表

特性 locals() globals()
作用范围 当前所在的局部空间 永远是整个程序的全局空间
返回内容 当前函数内的变量 全局变量 + 内置变量
在哪里用最准 函数内部 任何地方
能不能修改 通常只读 可以修改

5. 陷阱:能不能改?

globals() 可以改:

1
2
3
4
5
6
7
x = 10

def change():
globals()['x'] = 999

change()
print(x) # 999

locals() 改了通常没用(在函数里):

1
2
3
4
5
6
def test():
a = 1
locals()['a'] = 100
print(a) # 1,没变!

test()

6. 实际用途

1
2
3
4
5
6
7
8
9
10
11
12
# 用途一:调试打印
def calculate(a, b, c):
print(locals()) # 一次全打出来
return a + b + c

# 用途二:动态创建变量
for i in range(3):
globals()[f'var_{i}'] = i

print(var_0) # 0
print(var_1) # 1
print(var_2) # 2

7. 总结

locals() 看自己房间,globals() 看整栋大楼。
globals() 能改全局,locals() 通常只能看不能改。


三、装饰器(Decorator)

1. 定义

装饰器 = 一个接收函数、返回新函数的函数。

它让你不碰原函数,就能给它套上”日志、计时、权限、缓存”等外套。

2. 生活类比

  • 白T恤(原函数)→ 套一件印好图案的外套(装饰器)→ 原T恤没动,效果变了。

3. 最简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
def my_decorator(func):
def wrapper():
print("=== 开始执行 ===")
func()
print("=== 执行结束 ===")
return wrapper

@my_decorator # 等价于 say = my_decorator(say)
def say():
print("你好呀!")

say()

输出:

1
2
3
=== 开始执行 ===
你好呀!
=== 执行结束 ===

4. 装饰器模板

1
2
3
4
5
6
7
def 装饰器名(func):
def wrapper(*args, **kwargs):
# 【前戏】
result = func(*args, **kwargs)
# 【后戏】
return result
return wrapper

5. 装饰器的作用

功能 说明
日志记录 每次调用自动打印日志
性能计时 自动统计函数运行时间
权限校验 调用前检查用户是否登录
缓存结果 相同参数直接返回缓存
重试机制 失败自动重试 N 次
参数校验 检查参数是否合法

6. 实战例子:计时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time

def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时 {end - start:.4f} 秒")
return result
return wrapper

@timer
def slow_function():
time.sleep(2)

slow_function()

7. 实战例子:权限校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def login_required(func):
def wrapper(*args, **kwargs):
if not is_logged_in():
print("请先登录!")
return
return func(*args, **kwargs)
return wrapper

@login_required
def view_profile():
print("这是你的个人主页")

view_profile()
# 未登录时输出:请先登录!

逐行解释:

  • login_required(func):定义一个”门卫制造机”。
  • wrapper:包裹层,先检查登录,没登录就直接 return(拦截),登录了才放行。
  • @login_required 等价于 view_profile = login_required(view_profile)。
  • 调用 view_profile() 时,实际执行的是 wrapper()。

8. 实战例子:带参数的重试装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def retry(times):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第{i+1}次失败:{e}")
print("全部重试失败")
return wrapper
return decorator

@retry(times=3)
def unstable():
raise Exception("网络错误")

unstable()

输出:

1
2
3
4
第1次失败:网络错误
第2次失败:网络错误
第3次失败:网络错误
全部重试失败

关键理解:

  • 三层结构:retry(收参数)→ decorator(收函数)→ wrapper(干活)。
  • @retry(times=3) 等价于:
    1
    2
    temp = retry(times=3)
    unstable = temp(unstable)
  • return func(...) 成功时立刻返回,循环结束;失败则被 except 抓住,继续下一轮。
  • 循环全失败后才打印”全部重试失败”。

9. 使用场景

场景 说明
多个函数有相同的”前后处理” 记日志、计时、校验
框架开发 Flask 的 @app.route()、Django 的 @login_required
不想改原代码 原代码可能是别人写的
AOP(面向切面编程) 抽出日志、权限、缓存等横切关注点

10. 两个必知坑

坑1:原函数名字变了

1
print(say_hello.__name__)  # wrapper(不是 say_hello)

解决:用 functools.wraps。

坑2:多个装饰器执行顺序

1
2
3
@decorator_a
@decorator_b
def func(): ...

执行顺序:从下往上,先 b 再 a。

11. 总结

装饰器 = 接收函数、返回新函数。
@ 一放,代码干净;原函数不动,功能增强。

速记口诀:

传入函数,返回函数;中间夹心,增强功能;@ 一放,代码干净。


四、装饰器修复技术:functools.wraps

1. 问题:装饰器会偷走原函数身份

1
2
3
4
5
6
7
8
9
10
11
12
def my_decorator(func):
def wrapper():
func()
return wrapper

@my_decorator
def say_hello():
"""这是打招呼的函数"""
print("你好")

print(say_hello.__name__) # wrapper ❌
print(say_hello.__doc__) # None ❌

原因:@my_decorator 等价于 say_hello = my_decorator(say_hello),返回的是 wrapper。

2. 解决:@functools.wraps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import wraps

def my_decorator(func):
@wraps(func) # ← 加这一行
def wrapper():
func()
return wrapper

@my_decorator
def say_hello():
"""这是打招呼的函数"""
print("你好")

print(say_hello.__name__) # say_hello ✅
print(say_hello.__doc__) # 这是打招呼的函数 ✅

3. 复制了哪些属性

属性 含义
__name__ 函数名
__doc__ 文档字符串
__module__ 所属模块
__qualname__ 限定名
__dict__ 自定义属性
__wrapped__ 指向原函数

4. 使用模板

1
2
3
4
5
6
7
8
from functools import wraps

def 装饰器名(func):
@wraps(func) # 固定加这一行
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper

带参数的装饰器加在最内层:

1
2
3
4
5
6
7
def retry(times):
def decorator(func):
@wraps(func) # 加在这里
def wrapper(*args, **kwargs):
...
return wrapper
return decorator

5. 总结

装饰器会偷走原函数身份,@wraps(func) 是身份恢复器。

速记口诀:

装饰器一套,名字就丢掉;加个 @wraps,身份全找回。


五、语法糖(有参、无参)

1. 什么是语法糖

语法糖 = 让代码更好看、更好写的”糖衣”,本质不增加新功能。

2. 无参装饰器

1
2
@decorator
def func(): ...

等价于:

1
2
def func(): ...
func = decorator(func)

3. 有参装饰器

1
2
@decorator(arg1, arg2)
def func(): ...

等价于:

1
2
3
def func(): ...
temp = decorator(arg1, arg2)
func = temp(func)

4. 对比表

写法 含义
@decorator 直接装饰(无参)
@decorator(参数) 先传参,得到装饰器,再装饰(有参)

5. 其他常见语法糖

1
2
3
4
5
6
7
8
9
10
11
12
# 交换变量
a, b = b, a

# 列表推导式
result = [i * 2 for i in range(5)]

# with 语句
with open("a.txt") as f:
content = f.read()

# 链式比较
if 0 < x < 10: