跳到主要内容
Cowers://KNOWLEDGE
全部文章
Python

Python 进阶:闭包、装饰器、迭代器与生成器

通过连续的概念链理解闭包、装饰器、迭代器和生成器的实现原理与使用方式。

这四个概念层层递进:闭包是基础 → 装饰器建立在闭包之上 → 迭代器是一套协议 → 生成器是实现迭代器的简便方式。


一、闭包 Closure

是什么

嵌套函数 + 内层函数引用了外层变量 + 外层返回内层函数。 内层函数"记住"了它定义时所在的环境,即使外层函数已经执行完毕。

def make_counter():
    count = 0
    def counter():
        nonlocal count   # 修改外层变量必须声明 nonlocal
        count += 1
        return count
    return counter
 
c = make_counter()
print(c(), c(), c())   # 1 2 3

countmake_counter 返回后本该消失,但因为 counter 还引用着它,被"保活"了。

有什么用

核心价值:让函数携带私有状态,又不用写一个完整的类。

用途 说明
工厂函数 批量生产"定制版"函数
装饰器 装饰器全靠闭包记住被包装的函数
封装私有状态 外部完全访问不到,天然私有变量
延迟计算/回调 把"要做的事+数据"打包,等需要时再调用
# 工厂函数例子
def power(exp):
    def f(base):
        return base ** exp
    return f
 
square = power(2)   # square 记住了 exp=2
cube = power(3)     # cube 记住了 exp=3
print(square(5), cube(5))   # 25 125

⚠️ 经典坑:延迟绑定

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])   # [2, 2, 2]  ← 不是 [0, 1, 2]!

原因:闭包记住的是变量本身,不是变量当时的值。循环结束时 i 已经是 2,三个 lambda 调用时都去查 i,全得到 2。

创建阶段:造三个 lambda,每个都写着"返回变量 i"(不是值)
调用阶段:循环已结束,i = 2,三个 lambda 都返回 2

解法:用默认参数在定义时立即"冻住"值:

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])   # [0, 1, 2]  ✓

与 Java 对比

Java 匿名内部类捕获变量必须是 final(不可修改),Python 通过 nonlocal 允许修改捕获的变量。

💡 闭包是"穷人的对象",对象是"富人的闭包"。 状态简单时用闭包,状态复杂、方法多时用类。


二、装饰器 Decorator

作用:不修改原函数给原函数添加新功能

核心公式

@deco          等价于       def f(): ...
def f(): ...               f = deco(f)

@timer 就是 work = timer(work) 的语法糖,没有任何额外魔法。

理解步骤

第一步:函数可以当变量传递

def hello():
    print("hi")
 
f = hello    # 不加括号 = 函数本身(不执行)
f()          # 加括号 = 执行

第二步:函数可以接收函数、返回函数

def timer(func):          # ① 接收被装饰的函数
    def wrapper():        # ② 造一个新函数
        print("开始")
        func()            # ③ 调用原函数(闭包记住了 func)
        print("结束")
    return wrapper        # ④ 返回新函数(不加括号!)

第三步:@timer 自动执行替换

@timer
def work():
    print("干活")
# Python 自动执行:work = timer(work)
# work 这个名字现在指向 wrapper,原函数被 wrapper 包住了
 
work()
# 开始
# 干活
# 结束

完整写法(含 functools.wraps)

import functools
 
def timer(func):
    @functools.wraps(func)        # 把原函数的名字、文档等复制给 wrapper
    def wrapper(*args, **kwargs): # *args/**kwargs 接收任意参数
        import time
        start = time.time()
        result = func(*args, **kwargs)   # 原样转发参数,接住返回值
        print(f"{func.__name__} 耗时 {time.time()-start:.4f}s")
        return result             # 别忘了返回,否则原函数返回值丢失
    return wrapper

为什么要加 functools.wraps

# 不加的话:
print(work.__name__)   # 'wrapper'  ← 名字被覆盖了
print(work.__doc__)    # None        ← 文档也丢了
 
# 加了之后:
print(work.__name__)   # 'work'     ← 正常
print(work.__doc__)    # 原函数的文档

习惯:写装饰器时,给 wrapper 总加上 @functools.wraps(func)

带参数的装饰器(三层嵌套)

def repeat(n):              # 第一层:接收装饰器参数
    def decorator(func):    # 第二层:接收被装饰的函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs):   # 第三层:实际执行逻辑
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)          # 先执行 repeat(3) 得到 decorator,再装饰函数
def greet():
    print("hi")

多个装饰器的顺序

@timer
@logger
def work(): ...
 
# 等价于:
work = timer(logger(work))
# 离函数近的先套(内层),离函数远的后套(外层)

三、迭代器 Iterator

两个概念别混淆

实现了什么 能被 for 用 能被 next() 用 能重复遍历
可迭代对象 (list/str/dict) 只有 __iter__
迭代器 __iter__ + __next__ ❌(取完就空)

迭代器一定是可迭代对象,可迭代对象不一定是迭代器。

类比理解

可迭代对象 = 一本书(有内容,能被翻阅)
迭代器     = 书签(记着"翻到哪页了",能一页页往后翻)

你不能直接"翻书",先放书签(iter()),再靠书签一页页翻(next())。

底层机制

# for x in [1, 2, 3] 的真实过程
_iter = iter([1, 2, 3])     # 先拿迭代器
while True:
    try:
        x = next(_iter)     # 反复取下一个
        # 执行循环体...
    except StopIteration:   # 取完了就停
        break

手写迭代器类

class Countdown:
    def __init__(self, start):
        self.current = start
 
    def __iter__(self):
        return self              # 迭代器返回自身
 
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1
 
for x in Countdown(3):
    print(x)   # 3  2  1

迭代器 vs C++ 指针

Python 迭代器 C++ 指针/迭代器
移动方向 只能向前 可前后跳跃
地址概念 无,封装了位置 本质是内存地址
数据来源 可以"边走边生成" 必须指向已存在的内存

最大区别:Python 迭代器可以无中生有(惰性生成),不需要提前把数据放在内存里。


四、生成器 Generator

是什么

生成器 = 用 yield 实现迭代器的语法糖。不用写类,不用写 __iter____next__,函数里有 yield 就自动变成生成器。

调用生成器函数 ≠ 执行函数,而是造出一个迭代器对象。

def countdown(n):
    while n > 0:
        yield n      # 执行到这里暂停,把 n 返回出去
        n -= 1       # 下次 next() 才从这里继续
 
g = countdown(3)   # 一行代码都没执行!g 是生成器对象
print(g)           # <generator object countdown at 0x...>
 
print(next(g))     # 现在才开始执行,跑到 yield 3,暂停,返回 3
print(next(g))     # 从 n -= 1 继续,yield 2,暂停,返回 2
print(next(g))     # yield 1
print(next(g))     # while 不成立,StopIteration

yield vs return

return yield
执行后 函数结束,状态消失 函数冻住,状态保留
再次调用 从头开始 从上次暂停处继续
返回次数 只能一次 可以多次

核心特性:惰性求值

import sys
a = range(100_000_000)       # 惰性,不占内存
b = list(range(100_000_000)) # 真实列表,占几百 MB
 
print(sys.getsizeof(a))   # 48 字节
print(sys.getsizeof(b))   # 800+ MB

实用例子

例1:无限序列(普通列表做不到)

def fibonacci():
    a, b = 0, 1
    while True:           # 永远不停也没关系
        yield a
        a, b = b, a + b
 
fib = fibonacci()
for _ in range(8):
    print(next(fib), end=' ')   # 0 1 1 2 3 5 8 13

例2:大文件逐行处理

def read_lines(filename):
    with open(filename) as f:
        for line in f:
            yield line.strip()   # 每次只有一行在内存里
 
for line in read_lines("huge.log"):
    process(line)

例3:生成器表达式(把 [] 换成 ()

squares_list = [x*x for x in range(10)]   # 立刻算出全部,存进内存
squares_gen  = (x*x for x in range(10))   # 惰性,用时才算
 
# 直接传给函数,不需要先建列表
print(sum(x*x for x in range(10)))   # 285

例4:yield from 委托

def chain(*iterables):
    for it in iterables:
        yield from it   # 把 it 里的值一个个替我 yield 出去
 
print(list(chain([1, 2], [3, 4])))   # [1, 2, 3, 4]

何时用列表,何时用生成器

场景 用什么
需要多次遍历 列表 []
需要随机访问(索引) 列表 []
只遍历一次 生成器 ()
数据量大、节省内存 生成器 ()
无限序列 生成器(列表做不到)

五、总览:四个概念的关系

闭包
└── 让函数记住外层变量(基础能力)
 
装饰器
└── 吃进一个函数、吐出一个加强版函数
└── 靠闭包记住原函数
└── @语法糖 = f = deco(f)
 
迭代器协议
└── __iter__ + __next__
└── for 循环的底层机制
└── 取完就空,不可倒退
 
生成器
└── 用 yield 自动实现迭代器
└── 函数有 yield → 调用时返回生成器对象
└── 惰性求值,走一步生产一步

一句话串联:闭包让函数携带状态 → 装饰器用闭包包装函数 → 迭代器是"能逐个取值"的协议 → 生成器用 yield 轻松造出迭代器。