第3章 Python语言基础高频面试题

本章聚焦变量作用域、函数机制、面向对象、装饰器、生成器与GIL六大核心。每一题都附有回答框架与避坑指南,帮助你在面试中快速组织语言、展现深度。

回答框架核心 1. 变量作用域 LEGB / global/nonlocal 2. 函数机制 默认参数 / 闭包陷阱 3. 面向对象 MRO / 特殊方法 4. 装饰器/生成器 原理 / 内存优化 5. GIL 与并发 I/O vs CPU 任务

本章核心知识速览

主题高频考点典型题型
变量LEGB规则、global与nonlocal代码输出分析、闭包变量修改
函数默认参数陷阱、可变参数、闭包装饰器实现日志/权限
面向对象MRO、super、@property、__slots__设计模式实现、多重继承Diamond问题
装饰器函数装饰器、类装饰器、functools.wraps实现缓存装饰器、权限校验
生成器yield/next、生成器表达式、协程大数据流式处理、无限序列
GIL全局解释器锁、多线程 vs 多进程CPU密集场景如何提升性能

面试问题 1

Q:请解释 Python 的变量作用域规则(LEGB),并说明 global 和 nonlocal 的区别。

考察点:作用域链理解、闭包机制、面试者对 Python 变量查找顺序的掌握程度。

回答框架:

  1. 先说出 LEGB 全称并解释每层含义(Local → Enclosing → Global → Built-in)。
  2. 指出 global 用于在函数内修改全局变量,nonlocal 用于修改嵌套作用域(闭包外层)的变量。
  3. 举一个简单的嵌套函数例子说明 nonlocal 的使用场景。

简短示例回答: LEGB 指 Python 查找变量时依次搜索 Local(函数内部)、Enclosing(外层函数)、Global(模块级)、Built-in(内置作用域)。`global` 声明变量来自全局,允许修改;`nonlocal` 用于闭包中修改外层函数的局部变量,但不能影响全局。

可能追问:

  • 如果在嵌套函数中既想修改外层变量又想保留全局变量,应该怎么做?
  • 解释一下 locals() 和 globals() 的区别。

避坑提醒: 避免在函数内部直接对全局变量赋值而不使用 global,会创建新的局部变量而非修改全局。测试时注意嵌套作用域中 nonlocal 只能向上查找一层,不能跳跃。

面试问题 2

Q:Python 函数默认参数有哪些陷阱?请结合闭包解释 lambda 循环中变量捕获问题。

考察点:默认参数求值时机、可变对象作为默认参数的风险、闭包延迟绑定。

回答框架:

  1. 默认参数在函数定义时求值一次,如果参数是可变对象(如列表、字典),多次调用会共享同一对象。
  2. 闭包捕获的变量是引用而非值,lambda i=i: i 可以将当前值绑定到默认参数解决。
  3. 给出修复后的代码示例加深印象。

简短示例回答: 默认参数陷阱:`def f(a=[])` 每次调用都修改同一个列表。闭包陷阱:`lambdas = [lambda: i for i in range(3)]` 中所有 lambda 返回 2。解决:使用默认参数 `lambda i=i: i` 或 `functools.partial`。

可能追问:

  • 如何实现一个每次调用都生成新默认参数的装饰器?
  • functools.lru_cache 默认参数如何处理?

避坑提醒: 写代码时避免使用可变对象作为默认参数,改为 None 并在函数体内部初始化。闭包中如果用到循环变量,务必使用默认参数或 functools.partial 进行绑定。

面试问题 3

Q:谈谈装饰器和生成器的工作原理,以及 GIL 对多线程并发的影响。

考察点:Python 高级特性理解、生成器内存优化、GIL 局限性。

回答框架:

  1. 装饰器本质是函数,接收函数返回扩展后的函数;生成器使用 yield 暂停/恢复状态,按需生成值。
  2. GIL 让一个 Python 进程同一时刻只有一个线程执行字节码,CPU 密集任务无法利用多核。
  3. 应对方案:I/O 密集用多线程,CPU 密集用多进程或 C 扩展。

简短示例回答: 装饰器通过闭包实现函数增强,生成器通过 `yield` 实现惰性求值,节省内存。GIL 使得多线程无法真正并行计算,适合 I/O 密集型;CPU 密集型应使用 `multiprocessing` 或 `concurrent.futures.ProcessPoolExecutor`。

可能追问:

  • 如何用装饰器实现一个带参数的缓存(如 @cache(ttl=60))?
  • GIL 在 Python 3.12 中是否被移除?当前替代方案有哪些?

避坑提醒: 装饰器内层函数要使用 functools.wraps 保留元信息。生成器被 close() 后再次调用会抛出 StopIteration。不要认为 Python 多线程一定能加速,需先分析任务类型。


项目案例模板:实时用户行为采集与分析平台

维度内容
项目名称用户行为实时采集与清洗系统
业务背景支撑公司 200+ 产品线的行为数据接入,进行实时清洗、聚合,为运营提供分钟级报表。
规模指标日均处理 1.2 亿条事件,峰值 5k QPS;数据延迟 < 30s;存储 7 天原始数据(约 10TB)。
技术栈Python 3.9 + Flask(API 接收) + Celery + Redis(队列/缓存) + PostgreSQL(元数据) + Kafka(数据管道) + S3(冷存储)。
核心难点高并发下数据幂等性、重复消费避免;GIL 下 I/O 密集任务线程池调优;生成器流式读取大文件避免 OOM。
面试高频追问
  • 如何保证 Kafka 消费的幂等性?(配合 Redis 去重)
  • 大量生成器链式调用时如何优化内存?
  • GIL 下为何还用多线程?你们如何提升并发?(异步 IO + 多 worker 多进程)
  • 数据库写入瓶颈怎么处理?(批量写入 + 分表)
可迁移回答话术“我们采用生产者-消费者模式,用 Celery 任务队列解耦。遇到 GIL 瓶颈时,通过 multiprocessing 启动多个 worker 子进程,每个子进程内再使用协程处理 I/O。数据清洗使用生成器链,避免加载全部数据到内存,同时利用 yield from 实现 pipeline。最终通过 Redis 置位和数据库唯一约束保证幂等。”

💡 项目回答加分点: 主动提及用生成器解决内存问题、用多进程绕过 GIL、用 Redis 实现分布式锁去重,能体现你对 Python 底层机制的理解和工程落地能力。

⚠ 避坑提醒: 面试官如果追问 “GIL 下 Python 多线程到底有没有用” 时,不要只回答 “没用”。要区分 I/O 密集和 CPU 密集,并举例说明 threading 结合 asyncio 的混合方案。