
面试Python岗位十次有八次绕不开迭代器、生成器、装饰器。我做了几年Python开发也当过技术面试官这三兄弟几乎成了筛简历后的第一道关。很多候选人能背出概念定义但一问到底层机制就露馅了比如为什么for循环能遍历自定义对象、yield到底怎么暂停函数的、装饰器执行顺序是啥这些才是区分“背过题”和“真明白”的关键。这篇文章把这些年面试中被反复问到的题目、追问方式、以及背后的原理一次性梳理清楚。不管你是准备面试的求职者还是带实习生需要出题的组长都可以直接拿来用。我会从最基础的协议讲起逐步深入到高级用法和实战场景尽量把每个知识点的“为什么”也讲明白毕竟面试官最常干的事就是——你讲完一个知识点他紧接着问一句“为什么”。1. 面试官到底在考什么三个概念的底层逻辑先说一个很多教程没点破的事实迭代器、生成器、装饰器在Python里属于不同层面的东西但它们经常被放在一起问是因为这三者恰好覆盖了Python数据模型、函数式编程、语法糖实现三个核心区域。面试官问这些表面在考语法实际在考察你对Python执行机制的理解深度。1.1 三个概念之间的隐式关系迭代器和生成器是一对紧密相关的东西。生成器是迭代器的一种特殊形式但生成器的实现依赖于迭代器协议。装饰器则是另一条线它跟函数和类绑定得更深但高级用法里经常跟生成器配合使用。举一个典型的例子Python 3里的生成器表达式和列表推导式非常像但一个返回生成器对象一个返回列表。生成器对象就是一个迭代器它没有一次性把所有数据算出来而是用到一个算一个。这种“惰性求值”的思想在写数据处理代码时特别重要。装饰器则是一种“包装”思想。它不改变原函数的名字和调用方式却能在函数执行前后插入额外的逻辑。如果你理解了装饰器的本质是“把一个函数传给另一个函数再返回一个新的函数”那面试中百分之八十的装饰器题目都能解开。1.2 面试中的高频考察点分布根据我面试候选人时记录的提问频率这三块内容的考察点大致如下概念最高频考察点追问方向迭代器iter()和next()的配合、StopIteration异常如何自定义可迭代对象生成器yield的工作机制、惰性求值yield from、send()方法装饰器闭包、functools.wraps、带参数装饰器执行顺序、类装饰器你会发现面试官很少直接问“什么是迭代器”而是喜欢问“for循环是怎么遍历一个自定义类的”这种看似基础但极其致命的问题。一旦你答不上来for循环背后调用了iter()再不断调next()那就等于告诉面试官你写Python全凭IDE自动补全。2. 迭代器不只是next()这么简单迭代器这块面试题的核心就两个什么是可迭代对象、什么是迭代器、两者有什么关系以及如何自定义迭代器。2.1 迭代器协议与可迭代对象的区别先摆定义。可迭代对象Iterable是实现了__iter__()方法的对象或者实现了__getitem__()且按下标从0开始索引的对象。迭代器Iterator是实现了__iter__()和__next__()两个方法的对象。一个常见的面试陷阱是列表、元组、字典、集合是迭代器吗答案是不是它们是可迭代对象。每次调用iter([1, 2, 3])都会返回一个新的迭代器这个迭代器才有__next__()方法。为什么要有这个区分因为迭代器是有状态的它在内部记录了“我已经遍历到哪个位置了”。如果你把列表本身当作迭代器那么两个for循环同时遍历同一个列表就会互相干扰。所以Python的设计是可迭代对象是数据容器迭代器是遍历指针。我自己面试时会追问一个点for i in obj:这种语句Python解释器具体做了什么完整回答应该是三段先调用iter(obj)获取迭代器然后循环调用next(iterator)获取下一个元素直到捕获StopIteration异常退出循环。这个回答能体现你对for循环本质的理解。2.2 手写迭代器类的完整实现面试手撕代码时最经典的题目是“实现一个可以迭代的类”。比如写一个计数器从1数到n每次加1。这道题的满分答案是class Counter: def __init__(self, n): self.n n self.current 0 def __iter__(self): return self def __next__(self): if self.current self.n: raise StopIteration self.current 1 return self.current这里有一个关键细节__iter__返回self使得Counter既是可迭代对象又是自己的迭代器。这在某些场景下没问题但如果数据需要被多次遍历这种写法就有缺陷了。更规范的做法是让容器和迭代器分离——容器实现__iter__()返回一个新的迭代器对象迭代器对象实现__next__()。为什么面试官喜欢考这个因为很多候选人用过itertools但完全不知道里面那些工具是怎么实现的。能手写迭代器说明你对Python协议有真正的感知力。2.3 迭代器相关的经典面试追问迭代器还有一个高频考点迭代器的“一次性”问题。面试官可能会给你一段代码让你分析输出结果nums [1, 2, 3] iter1 iter(nums) iter2 iter(iter1) print(iter1 is iter2) # 输出什么答案是True。因为iter()函数在传入一个迭代器对象时会直接返回它本身而不是创建一个新的迭代器。这个特性叫作“迭代器的迭代器是自己”也是为什么for循环在多层嵌套时不会出问题。另一个常见追问是如何判断一个对象是可迭代对象还是迭代器答案是用collections.abc模块isinstance(obj, Iterable)和isinstance(obj, Iterator)。但有个坑生成器既是迭代器也是可迭代对象列表只是可迭代对象不是迭代器。还有个我特别喜欢问的面试题给定一个非常大的文件如何倒序读取最后几行常规思路是把文件全部读进内存然后切分但大文件会内存溢出。一个合格的回答是用deque配合迭代器实现固定长度的滚动窗口这其实也算迭代器思想的应用。3. 生成器面试中的性价比之王如果说迭代器考的是“懂不懂协议”那生成器考的就是“懂不懂惰性求值”。生成器是Python中最优雅的特性之一面试题基本围绕yield的工作机制和实际应用场景展开。3.1 yield关键字的工作机制详解生成器函数的核心是yield。遇到yield时函数返回值并暂停下次调用next()时从暂停位置继续执行。这个“暂停-恢复”机制是面试中必须讲清楚的点。我一般会画一个函数执行的流程对比普通函数从头执行到尾生成器函数可以执行到一半就停下来而且局部变量全部保留在“现场”。这个现场其实就是生成器对象的内部状态。一个经典的面试追问是生成器函数和普通函数有什么区别从源码层面看函数体内只要出现yield关键字Python在编译时就会将这个函数标记为生成器函数调用它时不执行任何函数体代码而是直接返回一个生成器对象。也就是说即使生成器函数里第一行有语法错误在调用函数时也不会报错只有真正迭代到那一行才会暴露。手撕代码最常见的题目是“用生成器实现斐波那契数列”。标准答案def fibonacci(): a, b 0, 1 while True: yield a a, b b, a b注意while True在这个场景下是合理的因为生成器是惰性的没人来取值就不会死循环。这也是生成器能让“无限序列”成为可能的原因。3.2 yield from和send()的进阶用法如果面试进行到高阶面试官会问yield from。这个语法是Python 3.3引入的用于在生成器中委托另一个生成器。def generator_a(): yield 1 yield 2 def generator_b(): yield 0 yield from generator_a() yield 3yield from的作用相当于展开内层生成器但它的真正价值在于打通了send()、throw()、close()这些方法的传递通道。面试中能答出这一点的人不多。send()方法也是高频考点。next(gen)等价于gen.send(None)而send(value)会把value作为yield表达式的结果传回生成器内部。这让生成器既能产出数据又能接收外部传来的数据变成一个双向通道。写一个能体现send()价值的例子def accumulator(): total 0 while True: value yield total if value is None: continue total value调用acc.send(10)会把10传给value然后更新total。这种协程雏形在某些异步框架里还能看到。面试时能主动提到“协程是基于生成器的send机制演变而来的”会显得知识面很扎实。3.3 生成器的经典面试场景题生成器在实战中的价值最典型的就是处理大数据。面试官常问有一个10GB的日志文件需求是统计每个IP出现的次数内存只有2GB怎么做最经济安全的回答是用生成器逐行读取文件然后进行流式处理避免一次把所有内容载入内存。def read_large_file(file_path): with open(file_path, r) as f: for line in f: yield line.strip()注意啊这里其实文件对象本身就支持迭代直接用for line in f就行但面试官想听到的是你具备“用生成器封一层”的意识。你可以在生成器里加入过滤、清洗、格式转换等逻辑实现职责分离。另一个经典题是“实现一个带有缓冲区的生成器每收集足够一行再输出”。这类题的考察重点是理解yield暂停后挂起状态的保存逻辑。还有一个基础知识要掌握生成器表达式。(x * x for x in range(10))返回的是生成器对象跟列表推导式相比不会一次性创建10个元素的列表。如果数据量是百万级这能节省不少内存。面试时可以顺带说一句“生成器表达式占用的内存几乎可以忽略因为它一次性只生成一个值”。4. 装饰器要讲透必须理解这几点装饰器是面试中的“重头戏”因为它考察的不只是语法还有你对函数式编程、闭包、Python对象模型的理解。面试官问装饰器的题目通常都不止一问会层层深入。4.1 闭包是装饰器的基础讲装饰器之前先必须讲闭包。闭包的定义很简洁——一个内层函数引用了外层函数的变量并且外层函数把这个内层函数作为返回值。这个内层函数连同它引用的外部变量就构成了闭包。def outer(x): def inner(y): return x y return inner这个例子中inner引用了outer的参数xouter返回了inner。你在外部拿到inner之后即使outer已经执行结束inner仍然记得x的值。闭包的关键在于Python的变量查找是LEGB规则内层函数找不到局部变量时会去外层函数的局部作用域找。而这个“外层局部作用域”在函数返回后并没有被销毁因为闭包持有它的引用。面试追问常是闭包有什么坑典型的是“循环变量陷阱”funcs [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # 输出 [2, 2, 2]因为lambda捕获的是变量i的引用不是i当时的值。循环结束后i的值是2所以每个lambda都返回2。解决办法是加默认参数lambda ii: i或者用functools.partial。4.2 手写通用装饰器模板装饰器的本质是语法糖decorator等价于func decorator(func)。所以手写通用装饰器的标准模板是import functools def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # 调用前逻辑 result func(*args, **kwargs) # 调用后逻辑 return result return wrapper这里有两个关键点必须理解不然面试必挂第一个是*args和**kwargs的作用。装饰器要装饰的函数参数完全未知所以wrapper必须能接收任意位置参数和关键字参数然后原样传递给被装饰函数。如果漏写了**kwargs被装饰函数一旦有关键字参数就会直接报错。第二个是functools.wraps(func)的意义。它把被装饰函数的元信息__name__、__doc__等拷贝到wrapper上。如果没有它函数的__name__就变成了wrapper调试和日志输出会非常混乱。面试里最常见的题是“写一个计算函数执行时间的装饰器”。答案就是用这个模板import functools import time def timer(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) end time.perf_counter() print(f{func.__name__} cost {end - start:.6f}s) return result return wrapper面试官很可能会追问为什么用time.perf_counter()而不是time.time()因为time.time()的精度受系统时间影响还可能被NTP同步修改不适合精确计时。而perf_counter()专门用于测量短时时间间隔精度最高是业内通用的计时方案。4.3 带参数的装饰器与functools.wraps带参数的装饰器比普通装饰器多一层嵌套。比如实现一个“可重试N次”的装饰器import functools def retry(max_attempts): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts 1): try: return func(*args, **kwargs) except Exception as e: if attempt max_attempts: raise print(fAttempt {attempt} failed: {e}, retrying...) return None return wrapper return decorator retry(max_attempts3) def unstable_api_call(): return ok注意这里有三层函数retry接收参数decorator接收被装饰函数wrapper接收运行时参数。很多候选人能写对普通装饰器但一写带参数的装饰器就少了一层。面试官还会问类装饰器的优缺点类装饰器可以借助类的__call__方法实现更复杂的带状态装饰逻辑class CountCalls: def __init__(self, func): self.func func self.calls 0 def __call__(self, *args, **kwargs): self.calls 1 return self.func(*args, **kwargs)类装饰器的优势是能通过实例属性保存状态不用依赖闭包。面试中答出这种写法会比较加分。4.4 装饰器面试真题缓存、重试、限流实战中还有几个常见的装饰器场景虽然不是语法难点但经常被用来考察“能不能把装饰器真正用到工程里”。缓存装饰器是最经典的。Python内置的functools.lru_cache就是一个带参数的装饰器import functools functools.lru_cache(maxsize128) def fib(n): if n 2: return n return fib(n-1) fib(n-2)面试官可能要求你手写一个简单的缓存装饰器通常用字典就能实现。这里要注意缓存key的设计简单场景下直接用args做key复杂场景要处理关键字参数和可变对象。限流装饰器也常被问到比如接口每秒最多调用N次import functools import time def rate_limit(max_calls, period): def decorator(func): call_times [] functools.wraps(func) def wrapper(*args, **kwargs): now time.time() call_times[:] [t for t in call_times if now - t period] if len(call_times) max_calls: raise RuntimeError(Rate limit exceeded) call_times.append(now) return func(*args, **kwargs) return wrapper return decorator这种题的目的就是让候选人灵活运用闭包状态和装饰器语法。能把上面几个写出来装饰器这一关基本能过。5. 三者联动面试中的综合应用题面试官出题有个习惯单一的语法点问完了就会出一些综合题把迭代器、生成器、装饰器串起来。这种题最容易暴露出候选人知识体系是否完整。5.1 装饰器生成器的组合场景一个典型的综合题是“写一个装饰器让生成器在迭代时自动记录日志”。先定义一个生成器比如逐行读取数据文件def read_data(): for i in range(100): yield i再定义一个装饰器包装这个生成器每当生成器产出一个值时打印日志import functools def log_generator_output(func): functools.wraps(func) def wrapper(*args, **kwargs): gen func(*args, **kwargs) for item in gen: print(fYielded: {item}) yield item return wrapper这里有个容易出错的点wrapper本身因为包含yield变成了生成器函数所以调用wrapper返回的不是原生成器的包装结果而是一个新的生成器对象。面试官如果追到这里基本就是在看你在迭代器和生成器之间能不能灵活转换。还有个更隐蔽的面试题在装饰器里怎么判断被装饰函数的返回值是不是迭代器可以用isinstance(result, Iterator)判断。这个看起来简单的题其实把两个概念贯通了装饰器操作函数返回值而迭代器协议定义了什么是可遍历对象。5.2 常见误区和性能坑三块知识结合起来之后有几个真实业务中很容易踩的坑。第一个坑生成器被复用后数据全没了。很多新人写代码时生成器对象保存下来觉得可以反复遍历结果第二次遍历发现是空的。因为生成器是一次性的遍历完就消耗完了。想反复遍历只能重新创建生成器对象。第二个坑装饰器改变了函数的元信息导致序列化失败。某些框架在传输函数时依赖函数名没加functools.wraps会导致函数名变成了wrapper轻则日志难排查重则框架内部映射关系错乱。第三个坑生成器配合for循环产生“慢半拍”的行为。面试中我出过这样一道题def gen(): print(start) yield 1 print(middle) yield 2 print(end) g gen() print(created) next(g) print(after first next)输出顺序是什么正确答案是created start after first next注意start是在第一次next()时才打印的说明生成器函数体在创建时根本没执行。这个点容易跟装饰器的“调用即执行”逻辑混在一起。装饰器包裹的函数在模块加载时就完成包装了而生成器函数是调用时返回生成器对象、迭代时才真正执行函数体。面试中能把这个区别讲得清清楚楚就能证明你是真的理解而不是背范文。6. 高频面试题速查与答题思路最后整理一份可以直接拿去复习和模拟的面试题清单每道题都附带答题要点和踩分关键。这部分不只是给求职者看的面试官也可以用来出题自查。6.1 面试题速查表题目核心考点标准答案要点for循环的内部实现迭代器协议先iter()再循环next()捕获StopIteration退出列表和生成器有什么区别惰性求值与内存占用列表一次性创建所有元素生成器逐个产出节省内存yield和return有什么区别生成器工作机制return结束函数并返回yield暂停函数并产出值装饰器执行顺序多个装饰器叠加装饰器栈从上往下装饰从下往上执行如何让一个装饰器同时支持有参和无参调用双模式兼容判断第一个参数是否为函数用callable()或利用*argsiter()调用一个迭代器会怎样迭代器协议返回自身iter(iter(obj)) is iter(obj)生成器的send(None)和next()之间的关系发送机制next(gen)等价于gen.send(None)send可以把值传给yield表达式结果多装饰器执行顺序这道题尤其要重点记住。看下面这个例子a b def func(): pass等价于func a(b(func))。执行时是先执行装饰器a传入的原函数b(func)即a先执行其外层逻辑然后调用b(func)最后执行func本身。所以装饰器的“执行顺序”和“定义顺序”是反的——定义时从上到下装饰运行时从下到上执行。这个考点几乎必考而且能区分出大量候选人。6.2 实战建议与个人体会根据我带实习生的经验这三部分知识要真正融会贯通不能只靠背题一定要自己动手写一遍。我建议的练习路径是先实现一个自定义迭代器类再把同一个逻辑改成生成器函数实现观察两者的区别最后用装饰器给这两个实现加上执行时间统计比较性能差异。整个过程做完面试时基本不会有太大问题。实际业务中还有一个被很多人忽视的点itertools标准库是这三块知识的集大成者。itertools.chain的原理就是迭代器的串联itertools.islice就是生成器的切片itertools.lru_cache则是装饰器的典型应用。面试前花时间读一遍itertools源码比刷十道面试题都管用。最后分享一个我面试时常用的“六字心法”对象、协议、语法糖。迭代器的核心是协议设计生成器的核心是惰性计算装饰器的核心是函数式包装。抓住这三条主线无论面试题怎么变你都能快速找到对应的知识坐标用底层原理去拆解题目而不是生搬硬套答案。这套理解方式也是我在实际写代码时判断一个候选人是否具备长期成长潜力的关键依据。