九、迭代器/生成器/装饰器/GIL 与 numpy/pandas/正则/模块(P73–P85)
P73 · 知识点:生成器与 yield
【题目】yield 关键字的作用是? A. 暂停函数执行并返回一个值,下次调用从暂停处继续 B. 结束函数 C. 抛出异常 D. 开启新线程
答案:A
【考点】生成器机制:yield 保存执行状态,惰性求值。
【结论】选 A。yield 关键字可以使生成器函数暂停执行并返回一个值,下次调用时从暂停处继续执行,实现惰性求值。
【推导过程】
def gen():
yield 1
yield 2
g = gen() # 调用生成器函数,返回生成器对象,不执行函数体
print(next(g)) # 输出 1,执行到第一个 yield 后暂停
print(next(g)) # 输出 2,从暂停处继续,执行到第二个 yield 后暂停【逐项辨析】
- A. 正确。 含 yield 的函数为生成器函数,调用时返回生成器对象而非立即执行;每次迭代执行到 yield 时暂停并返回该值,同时保存局部变量与执行位置,下次从暂停处继续。
- B. 错误。 结束函数应使用 return,return 会终止函数并返回值;yield 是暂停并保存状态,二者语义完全不同。
- C. 错误。 抛出异常应使用 raise,yield 与异常机制无关。
- D. 错误。 开启新线程应使用 threading.Thread,yield 不涉及任何线程调度或并发原语。
【知识点】 生成器(Generator)是 Python 中一种特殊的迭代器,通过生成器函数(包含 yield 的函数)或生成器表达式创建。其核心机制包括:
- 惰性求值(Lazy Evaluation):生成器不会一次性把所有结果加载到内存,而是按需逐个产生值,非常适合处理大规模数据流或无限序列。
- 状态保存:每次 yield 时,Python 解释器会保存函数的局部变量、指令指针等执行状态,封装在生成器对象中;下次调用
__next__()时恢复现场继续执行。 - 生成器对象:调用生成器函数得到的是生成器对象,它实现了迭代器协议(
__iter__和__next__),因此可直接用于 for 循环或 next() 函数。 - 双向通信:Python 的 yield 不仅是输出通道,通过
send(value)方法还可向生成器内部发送数据,实现协程(Coroutine)效果。例如:
def accumulator():
total = 0
while True:
val = yield total
if val is None:
break
total += val- 内存优势:与列表推导式相比,生成器表达式
(x for x in range(10**8))几乎不占用额外内存,而列表推导式[x for x in range(10**8)]会瞬间消耗大量 RAM。
【记忆锚点】 “yield 一停一等,省内存、惰性行;暂停处接着跑,生成器是宝。”
【易混对比】
| 对比维度 | yield | return | 生成器 | 列表 |
|---|---|---|---|---|
| 执行行为 | 暂停并保存状态 | 终止函数并返回 | 惰性求值,逐项产出 | 一次性全部创建 |
| 内存占用 | 极低(状态机) | 无(单次返回) | 低(O(1) 额外空间) | 高(O(n) 空间) |
| 可否多次返回 | 可以(多次 yield) | 一次(函数结束) | 可以(无限序列) | 已固定长度 |
| 适用场景 | 大数据流、无限序列 | 普通函数结果返回 | 遍历大文件、流水线处理 | 需要索引访问的小数据 |
换问法:若题目改为“以下哪个关键字可以将普通函数转换为生成器函数?”,答案仍是 yield;若改为“生成器与列表推导式的核心区别是什么?”,则应答“惰性求值与内存占用”。
【自测】 以下代码的输出结果是什么?
def countdown(n):
while n > 0:
yield n
n -= 1
print(sum(countdown(3)))答:输出为 6。countdown(3) 依次产生 3、2、1,sum 求和为 6。生成器函数在每次 yield 后暂停,循环变量 n 的状态被保留,直至 n 不大于 0 时自然结束。与第 P74 题连考。
【知识关联】
- 同库关联:与 P17(生成器表达式)、P50(map 惰性)。
- 实现层:yield 保存帧状态;
send/throw/close;内存 O(1)。 - 面试追问:① 生成器与列表内存差异?② 如何中断?
【拓展延伸】
- 变式问法:yield from;生成器表达式 vs 生成器函数。
- 版本差异:3.3 yield from;异步生成器。
- 工程注意点:大文件/流式处理优先生成器;注意一次性消费。
P74 · 知识点:装饰器
【题目】@decorator 装饰器语法等价于?
A. func = decorator(func) B. func = decorator() C. 没有实际效果 D. decorator = func(decorator)
答案:A
【考点】装饰器的本质:接收函数、返回增强函数的高阶函数。
【结论】选 A。@decorator 是 Python 提供的语法糖,其本质等价于 func = decorator(func)。
【推导过程】
# 装饰器语法糖
@decorator
def func():
pass
# 完全等价于
def func():
pass
func = decorator(func)【逐项辨析】
- A. 正确。 装饰器本质上是高阶函数(Higher-Order Function),它接收一个函数对象作为参数,并返回一个新的、增强后的函数对象;语法糖
@decorator只是将这一赋值过程简化。 - B. 错误。
decorator()表示调用装饰器函数本身(不传参),常用于带参数装饰器的最外层工厂函数;无参数装饰器的正确等价形式不是 decorator()。 - C. 错误。 装饰器具有明确的实际效果,它能在不修改被装饰函数源代码的前提下,为其增加日志记录、权限校验、缓存、性能计时等横切关注点功能。
- D. 错误。
decorator = func(decorator)将原函数作为工厂去“装饰”装饰器,完全颠倒了二者的主从关系,不符合装饰器的设计语义。
【知识点】 装饰器(Decorator)是 Python 中实现面向切面编程(AOP)的核心语法特性,其底层依赖以下机制:
- 高阶函数:Python 中函数是一等公民(First-Class Citizen),可以像普通变量一样被传递、赋值和返回。装饰器正是利用这一特性,接收函数并返回函数。
- 闭包(Closure):装饰器内部通常定义嵌套函数(wrapper),该嵌套函数引用了外部函数的局部变量(被装饰的函数对象),从而形成闭包,确保被装饰函数在后续调用时能够正确执行增强逻辑。
- 语法糖解析:Python 解释器在编译阶段遇到
@decorator时,会将其解析为函数定义后的重新绑定。若存在多层装饰器,如:
@a
@b
def f(): ...则等价于 f = a(b(f)),执行顺序为自下而上。 4. functools.wraps:由于装饰器返回的新函数会丢失原函数的元数据(如 __name__、__doc__),标准库提供 functools.wraps 装饰器来拷贝这些属性,保证内省(Introspection)正确性。 5. 带参数装饰器:当装饰器本身需要配置参数时,通常采用三层嵌套结构——外层函数接收参数并返回真正的装饰器,中层装饰器接收被装饰函数,内层 wrapper 接收实际调用参数:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator【记忆锚点】 “@ 符号一贴脸,func = 装饰器(func)记心间;高阶函数包一层,不改动源码功能添。”
【易混对比】
| 对比维度 | 无参数装饰器 | 带参数装饰器 | 类装饰器 |
|---|---|---|---|
| 语法示例 | @decorator | @decorator(arg) | @ClassDecorator |
| 等价形式 | f = decorator(f) | f = decorator(arg)(f) | f = ClassDecorator()(f) |
| 嵌套层数 | 2 层(decorator + wrapper) | 3 层(工厂 + decorator + wrapper) | __init__ + __call__ |
| 典型用途 | 日志、权限 | 缓存配置、重试次数 | 状态管理、注册表 |
换问法:若题目改为“多层装饰器 @a @b def f() 的执行顺序是怎样的?”,答案应为“自下而上,等价于 f = a(b(f))”;若改为“如何保留被装饰函数的 __name__ 和 __doc__?”,则应答“使用 functools.wraps”。
【自测】 以下装饰器的作用是什么?写出 greet("World") 的输出。
from functools import wraps
def uppercase(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase
def greet(name):
return f"hello, {name}"答:输出为
HELLO, WORLD。uppercase 装饰器将原函数的返回值转换为大写;@wraps(func)确保 wrapper 拷贝了 greet 的元数据。与第 P75 题连考。
【知识关联】
- 同库关联:与 P48(闭包)、P58(魔法方法);
functools.wraps。 - 实现层:语法糖
@dec等价f = dec(f);可带参装饰器两层函数。 - 面试追问:① 为何要 wraps?② 多个装饰器顺序?
【拓展延伸】
- 变式问法:带参数装饰器;类装饰器。
- 版本差异:语义稳定。
- 工程注意点:日志/鉴权/缓存常用;保持 wraps 元数据;避免过深嵌套。
P75 · 知识点:GIL
【题目】关于 CPython 的 GIL(全局解释器锁),下列说法正确的是? A. GIL 使得多线程无法利用多核并行执行 CPU 密集型任务 B. GIL 保证了多线程代码绝对安全 C. GIL 只存在于 Jython 中 D. 多线程一定比单线程快
答案:A
【考点】GIL 对多线程的影响及适用场景。
【结论】选 A。CPython 的 GIL 导致同一时刻仅有一个线程执行字节码,因此 CPU 密集型多线程无法真正并行利用多核。
【逐项辨析】
- A. 正确。 GIL(Global Interpreter Lock)是 CPython 解释器的全局互斥锁,任何时刻只允许一个线程持有该锁并执行 Python 字节码;对于 CPU 密集型任务,多线程只能串行执行,无法在多核 CPU 上并行加速。
- B. 错误。 GIL 保护的是解释器级别的字节码执行原子性(主要为了简化 C 扩展的内存管理与引用计数),并不保证用户代码层面的线程安全;若多个线程操作共享可变数据,仍需使用 Lock、RLock、Semaphore 等同步原语。
- C. 错误。 GIL 是 CPython 的实现细节,Jython(基于 JVM)和 IronPython(基于 .NET)没有 GIL,它们依赖宿主虚拟机的线程调度机制。
- D. 错误。 多线程在 CPU 密集型场景下由于 GIL 争抢与线程切换开销,往往比单线程更慢;只有在 IO 密集型场景(如网络请求、文件读写)中,线程在等待 IO 时会释放 GIL,此时多线程才能提升整体吞吐量。
【知识点】 GIL 是 CPython 架构中最具争议的设计之一,深入理解其原理与影响至关重要:
- 设计动机:CPython 使用引用计数(Reference Counting)进行内存管理,对象的生命周期由
ob_refcnt字段决定。若没有 GIL,多线程并发地修改引用计数将导致竞态条件与内存泄漏/重复释放。GIL 以粗粒度锁的方式简化了 C 扩展开发,避免了为每个对象加细粒度锁的巨大开销。 - 释放时机:CPython 3.2 起改为时间片制——线程拿到 GIL 后可以一直执行,直到用完一个「切换间隔」(默认 5 毫秒)才主动释放,让其他线程竞争;执行 IO 操作(read、write、sleep)或进入 C 扩展的释放段时也会提前交出。间隔用
sys.getswitchinterval()/sys.setswitchinterval()读写,本机 Python 3.12.3 实测sys.getswitchinterval()返回0.005。注意口径:「累计约 5000 条字节码指令后释放」是 Python 2 时代sys.setcheckinterval()的老规则,3.2 已换成时间片、3.5 起setswitchinterval取代setcheckinterval(本机 3.12.3 实测hasattr(sys, "setcheckinterval")为 False),与本题【拓展延伸】的版本差异一致,不要再按"条指令"表述。 - CPU 密集型瓶颈:对于计算密集型任务(如数值计算、图像处理),由于 GIL 的存在,多线程无法并行,此时应使用
multiprocessing模块创建独立进程,每个进程拥有独立的 Python 解释器与 GIL,从而利用多核。 - 替代方案:
- 多进程(multiprocessing):绕过 GIL,适合 CPU 密集型,但进程间通信开销较大。
- C 扩展释放 GIL:在 C/C++ 扩展中(如 NumPy),可在纯计算阶段显式释放 GIL,让其他 Python 线程并行执行。
- asyncio:单线程协作式多任务,适合高并发 IO 场景,避免线程切换开销。
- 子解释器(PEP 554):Python 3.12+ 引入的子解释器可在同一进程内运行多个独立解释器,各自拥有 GIL。
- 无 GIL 构建(PEP 703,Python 3.13+):实验性的
--disable-gil编译选项,采用更细粒度的锁与引用计数策略(如偏向引用计数、immortal 对象等),彻底移除 GIL,但可能导致单线程性能下降约 10%。
- 适用场景选型:IO 密集型首选多线程或 asyncio;CPU 密集型首选多进程或 C 扩展;高并发网络服务首选 asyncio。
【记忆锚点】 “CPython 有把 GIL 锁,CPU 并行靠进程过;IO 等待会解锁,多线程在此时显卓。”
【易混对比】
| 对比维度 | GIL | threading.Lock | multiprocessing |
|---|---|---|---|
| 作用范围 | 整个解释器(全局) | 用户代码中的特定代码块 | 独立进程,独立解释器 |
| 保护对象 | 字节码执行 + 引用计数 | 用户共享资源 | 进程地址空间隔离 |
| 多核利用 | 无法利用(CPU 密集) | 无法突破 GIL 限制 | 可以完全利用多核 |
| 适用场景 | CPython 内部机制 | 线程间共享数据保护 | CPU 密集型计算 |
| 开销 | 解释器内部自动管理 | 轻量级(用户显式加锁) | 较重(进程创建与 IPC) |
换问法:若题目改为“在 CPython 中,如何使 CPU 密集型任务利用多核并行?”,答案应为“使用 multiprocessing 模块或编写释放 GIL 的 C 扩展”;若改为“Python 3.13 提供了什么实验性特性来移除 GIL?”,则应答“无 GIL 构建(--disable-gil)”。
【自测】 以下代码运行在多核 CPU 的 CPython 环境中,执行 cpu_bound() 时能否利用多核?如何改进?
import threading
def cpu_bound():
count = 0
for i in range(10**8):
count += 1
return count
threads = [threading.Thread(target=cpu_bound) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()答:不能利用多核。由于 GIL 的存在,四个线程只能串行执行字节码,实际仅使用一个核心,且线程切换带来额外开销。改进方案:将 threading.Thread 替换为 multiprocessing.Process,每个进程拥有独立 GIL,即可实现四核并行。与第 P73 题连考。
【知识关联】
- 同库关联:与 J51–J62(Java 真并行:线程、线程池、CAS、volatile)对照记忆——Java 线程映射 OS 线程,CPU 密集可多核并行;与 P73–P74(生成器/迭代器/装饰器)同属 Python 运行时进阶;与 P76 之后的数据处理题群也相关:numpy/pandas 底层 C 实现在计算段释放 GIL,故「Python 多线程 + numpy」有时仍能吃满多核。
- 实现层:CPython 解释器执行字节码前必须持有 GIL;引用计数 ob_refcnt 的增减若无全局互斥,在多线程下会竞态(过早释放或泄漏)。GIL 的获取/释放发生在字节码指令边界与明确的 IO/C 扩展出口;sys.setswitchinterval 可调切换间隔。多进程方案下每个子进程独立解释器与 GIL,无共享堆,IPC 走 pickle/Queue/Pipe/共享内存。Jython/IronPython 无 GIL,依赖 JVM/.NET 线程;PyPy 有 GIL(另有 STM 实验分支);CPython 3.13 的 free-threaded 构建通过细粒度锁与 biased reference counting 等策略移除 GIL,单线程可能略慢。
- 面试追问:① GIL 保护的是什么、不保护什么?② 为何 IO 密集多线程仍然有效?③ 如何让 CPU 密集任务利用多核?④ time.sleep 期间 GIL 会怎样?(释放)⑤ 持 GIL 再等用户 Lock 时要注意什么?
【拓展延伸】
- 变式问法:① 多线程爬虫 vs 多进程矩阵乘法,问能否加速;② 问 Python 3.13 移除 GIL 的实验特性名称(--disable-gil / free-threaded);③ 问「在 C 扩展计算循环中其他 Python 线程能否运行」(若未释放 GIL 则不能)。
- 版本差异:GIL 在 CPython 历史上长期存在;sys.setswitchinterval 自 3.5 替代旧的 setcheckinterval;3.8+ 持续优化切换与子解释器;3.12 强化子解释器方向;3.13 提供实验性 free-threaded 构建(PEP 703),3.14+ 继续打磨生态(部分 C 扩展需适配);numpy/scipy 等科学计算库本身在 C/Fortran 计算段释放 GIL,与多线程叠加时需看具体 API。
- 工程注意点:① 生产 CPU 密集用 multiprocessing/ProcessPoolExecutor,注意 Windows 下 main 保护与 pickle 限制;② IO 密集用线程或 asyncio,不要为 CPU 任务堆线程;③ 压测时区分 GIL 瓶颈与锁瓶颈;④ 监控线程切换与上下文成本;⑤ 若依赖 free-threaded 构建,必须锁定依赖版本并完整回归 C 扩展。
P76 · 知识点:numpy 数组创建与 shape
【题目】执行下列代码后,a.shape 的值是?
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])A. (6,) B. (2,) C. (2, 3) D. (3, 2)
答案:C
【考点】numpy 多维数组的 shape 属性与维度含义。
【结论】选 C。数组为 2 行 3 列,shape 为 (2, 3)。
【逐项辨析】
- A 错误:
(6,)是展平后的一维长度。 - B 错误:
(2,)只表示最外层有 2 个元素。 - C 正确:外层长度 2(行),内层长度 3(列)。
- D 错误:
(3, 2)是转置后的形状。
【知识点】 numpy 数组的核心是「同构、连续、带 shape 元数据的缓冲区」。创建方式包括:np.array([[1,2,3],[4,5,6]])(从嵌套序列)、np.zeros/ones/empty((2,3))、np.arange(6)、np.linspace(0,1,5)、np.eye(3) 等。 关键属性四件套:
- shape:各维长度元组,本题外层 2 个子列表、每个 3 个元素,故 (2, 3);
- ndim:维数,len(shape);
- size:元素总数,各维乘积;
- dtype:元素类型(int64/float64/bool/...),全数组统一。 与 Python list 的本质差异:list 可异构、嵌套可不齐、+ 是拼接;ndarray 强制同 dtype,运算走向量化 ufunc(底层 C 循环),支持形状广播。常见坑:np.array([1,2,3]).shape 是 (3,)(一维);[[1],[2],[3]] 是 (3,1)(列向量);转置 .T 或 .reshape(3,2) 会得到 (3,2)。运算前先对齐 shape 是数据代码的第一道纪律。
【记忆锚点】「shape 从外到里:先行后列;几层嵌套就是几维。」
【易混对比】
len(a)对二维数组返回行数,不是元素总数。a.shape = (3,2)可原地变形;reshape返回新数组对象。
【自测】np.zeros((2, 3, 4)) 的 shape、ndim、size 分别是多少?
答:shape=(2,3,4),ndim=3,size=24。与 P78 连考。
【知识关联】
- 同库关联:与 P77(切片视图)、P78(广播)、P79(pandas 底层基于 numpy)构成数据处理题群;与 P12/P13(list 切片与负索引)对照——numpy 的 shape/轴语义是向量化计算的基础。银行/数据岗 Python 笔试常把 shape、dtype、reshape 连考。
- 实现层:np.array 从嵌套序列推断维度:列表的列表层数 = ndim;shape 各维长度 = 各层长度。数组在内存中是连续(或带 stride 的)缓冲 + dtype 描述 + shape/strides 元数据;size = prod(shape),nbytes = size * itemsize。np.zeros((2,3,4)) 先分配 24 个元素再填 0;dtype 默认由输入推断。reshape 不改变数据缓冲,只改 shape/strides(元素总数必须一致);a.shape = (3,2) 为原地修改属性。与 Python list 不同:list 可异构、嵌套不齐;numpy 强制同 dtype,运算向量化、底层 C 循环。
- 面试追问:① shape 与 ndim、size 的关系?② np.array([[1,2,3],[4,5]])(不等长)会怎样?(直接报错,不再有 object 数组分支:numpy 2.4.6 实测
ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (2,) + inhomogeneous part.;想装异构嵌套必须显式dtype=object,此时得到 shape=(2,) 的一维对象数组)③ 如何创建全 1 的三维数组并查看类型? 【拓展延伸】 - 变式问法:① 问 np.arange(6).reshape(2,3).shape((2,3));② 问 np.array(1).shape((),零维标量数组);③ 给出多层嵌套列表问 ndim。
- 版本差异:numpy 1.x → 2.x 调整了部分标量类型提升、np.float_ 等别名移除、字符串与 promotion 规则,但 shape/ndim/size 语义稳定;np.matrix 仍可用但官方不推荐;np.typing 便于类型标注。
- 工程注意点:① 读表后先 df.to_numpy() 再算时注意 dtype 统一;② 大数组警惕意外副本导致内存翻倍;③ 指定 dtype=np.float32 可省一半内存(精度允许时);④ shape 与业务含义(样本×特征)写进注释;⑤ 单测断言 arr.shape 防止转置错误。
P77 · 知识点:numpy 切片是视图
【题目】执行下列代码后,a 的内容是?
import numpy as np
a = np.array([1, 2, 3, 4])
b = a[1:3]
b[0] = 99
print(a)A. [1, 2, 3, 4] B. [1, 99, 3, 4] C. [99, 2, 3, 4] D. 报错,切片不能赋值
答案:B
【考点】numpy 切片默认返回视图(view),与 Python list 切片不同。
【结论】选 B。b = a[1:3] 得到视图,修改 b 会写回 a。
【逐项辨析】
- A 错误:若
b是独立拷贝才会得到原数组不变。 - B 正确:基础切片返回 view,共享底层缓冲区。
- C 错误:切片从索引 1 开始,不是 0。
- D 错误:切片结果可正常赋值。
【知识点】 Python list 切片 b = a[1:3] 会创建新列表(浅拷贝),改 b 不影响 a。numpy 基础切片 a[start:stop:step] 则返回视图(view):新 ndarray 对象有自己的 shape/strides/offset 元数据,但 base 仍指向同一块数据缓冲;因此 b[0] = 99 会写回原数组 a。 区分视图与拷贝的规则:
- 基础切片(冒号语法、整数区间/步长)→ 视图,共享内存;
- 高级索引(布尔掩码 a[a>2]、整数数组 a[[0,2]])→ 拷贝,独立缓冲;
- 显式拷贝:.copy() 或 np.array(a[1:3])。 检测手段:np.shares_memory(a, b)(推荐)或 b.base is a(结构复杂时 base 可能是中间对象)。安全写法是需要独立修改时先 .copy()。赋值 a[1:3] = [0,0] 是通过视图写原数组,与「得到拷贝再改」语义不同。工程里「改切片污染原表」是数据分析事故高发点,与 P67–P72 的浅拷贝陷阱同源。
【记忆锚点】「numpy 切片是窗户,改窗户就是改屋里;list 切片像照片。」
【易混对比】
| 操作 | numpy | Python list |
|---|---|---|
b = a[1:3] | 视图 | 新列表(浅拷贝) |
修改 b[0] | 影响 a | 不影响 a |
【自测】 如何使修改 b 不影响 a?原代码 np.shares_memory(a, b) 返回什么?
答:
.copy();不 copy 时返回 True。与 P67 连考。
【知识关联】
- 同库关联:与 P67–P72(浅拷贝/deepcopy/可变性陷阱)是同一考点在 numpy 的映射;与 P12(list 切片副本)对照记忆差异;与 P76(创建与 shape)衔接。数据分析里「改了切片污染原表」是高频事故。
- 实现层:numpy 基础切片 a[start:stop:step] 返回 view:新数组对象携带自己的 shape/strides/offset,但 base 指向同一数据缓冲,b.flags["OWNDATA"] 为 False。高级索引(布尔掩码、整数数组)返回 copy。b[0]=99 通过 strides 定位到 a 的对应元素并原地写入。检测:np.shares_memory(a,b) 或 b.base is a(结构更复杂时 base 可能是中间对象)。强制拷贝:b = a[1:3].copy() 或 np.array(a[1:3])。注意:b = a[1:3] > 2 得到的是新布尔数组(copy);a[1:3] = [0,0] 是切片赋值,写的是原数组。
- 面试追问:① 何时 numpy 会隐式 copy?② 如何在函数传参时避免污染调用方数组?③ b = a[:] 与 b = a.copy() 区别?(前者仍可能是 view) 【拓展延伸】
- 变式问法:① b = a[a>2]; b[0]=0 问 a 是否变化(否);② b = a.reshape(2,2) 后修改 b 问 a(通常仍是 view,共享内存);③ 问 np.shares_memory 返回值。
- 版本差异:视图/拷贝语义在 numpy 1.x/2.x 稳定;2.x 对某些索引返回的 writeable 标志更严格;pandas 2.x 的 copy-on-write(CoW)默认化后,DataFrame 切片行为更接近「改副本」,与 numpy view 形成对比,迁移时要注意。
- 工程注意点:① 预处理管线中「是否原地修改」写清 API 约定(加 _inplace 或统一返回新对象);② 共享内存处用 assert not np.shares_memory 做防御性测试;③ 大数组 copy 有成本,能 view 则 view,但要防意外写;④ 与 pandas 混用时注意 df.values/to_numpy() 的视图语义随版本变化。
P78 · 知识点:numpy 广播机制
【题目】执行下列代码后,c 的结果是?
import numpy as np
a = np.array([[1], [2], [3]]) # shape (3, 1)
b = np.array([10, 20]) # shape (2,)
c = a + bA. 报错,形状不匹配无法相加 B. [[11], [22], [30]] C. 一维数组 [11, 22, 3] D. [[11, 21], [12, 22], [13, 23]]
答案:D
【考点】numpy 广播(broadcasting)规则。
【结论】选 D。(3,1) 与 (2,) 按广播对齐为 (3,2),对应元素相加。
【逐项辨析】
- A 错误:符合广播规则,不会报错。
- B 错误:这需要
b形状为(3,1)才可能类似对齐。 - C 错误:广播结果保持二维,且数值不对。
- D 正确:
a广播为[[1,1],[2,2],[3,3]],加b得该结果。
【知识点】 numpy 广播(broadcasting)定义了不同 shape 数组如何对齐后做逐元素运算,避免显式写 Python 循环。规则从最右维向左逐维对齐:
- 维度不足的一方在左侧补 1;
- 每一维要求长度相等,或其中一方为 1,否则抛 ValueError: operands could not be broadcast together;
- 长度为 1 的维在逻辑上被「拉伸」到对侧长度;结果 shape 取各维最大值。 本题:a.shape=(3,1),b.shape=(2,) → b 视作 (1,2) → 对齐结果 (3,2)。a 逻辑扩成 [[1,1],[2,2],[3,3]],b 扩成三行相同的 [10,20],相加得 [[11,21],[12,22],[13,23]],对应选项 D。 实现上并不真复制大块数据:长度 1 的维用 stride=0 模拟拉伸。同一套规则适用于 + - * / 与 np.maximum 等 ufunc;@ / dot 是矩阵乘,规则不同。与 list 的 +(拼接)务必区分;与 pandas 的标签对齐也是不同概念。
【记忆锚点】「右对齐,缺的补一,一的拉伸;对不上就报错。」
【易混对比】
*是对应元素运算;@/dot才是矩阵乘。- Python list 的
+是拼接,不是逐元素相加。
【自测】np.ones((2,3)) * np.array([1,2,3]) 的 shape 与内容?
答:shape=(2,3);两行均为
[1,2,3]。与 P76 连考。
【知识关联】
- 同库关联:与 P76(shape)、P79(pandas 对齐)、P08(
"5"+3报错:列表的+才是拼接,字符串与数字相加会 TypeError)对照;数据清洗/特征工程中广播用于标准化、归一化、加偏置列。笔试常给两段数组问 +/* 后的 shape。 - 实现层:广播不真正复制大规模数据,而是把「长度为 1 的维度」的 stride 视作 0,使底层循环假装该维被拉伸。规则(从最右维向左):① 维度不足的一方左侧补 1;② 每维长度相等,或其中一方为 1,否则报 ValueError: operands could not be broadcast together;③ 结果 shape 取各维最大长度。本题 (3,1)+(2,) → b 补成 (1,2) → 对齐得 (3,2):a 广播为 [[1,1],[2,2],[3,3]],b 广播为三行相同的 [10,20],相加得 [[11,21],[12,22],[13,23]]。*、-、/ 与 np.maximum 等 ufunc 都遵守同一套广播规则;@/dot 是矩阵乘,规则不同。
- 面试追问:① (3,1)+(1,4) 的 shape?((3,4))② (2,3)+(3,) 为何可以?③ 如何避免广播造成的隐式大结果(内存)? 【拓展延伸】
- 变式问法:① 计算 np.ones((2,3,1)) + np.zeros((3,2)) 是否可广播(看对齐:最右 1 与 2,前一维 3 与 3,再前 2 与补 1 → (2,3,2) 可以);② 问 list 的 [1,2]+[3,4](拼接)与 numpy 差异;③ 给出错误 shape 问报错原因。
- 版本差异:广播规则多年稳定;numpy 2.x 增强错误信息(更清晰地打印 shape);np.broadcast_shapes/np.broadcast_arrays 便于预先检查;numba/PyTorch/TensorFlow 的张量广播语义与 numpy 对齐(PyTorch 从尾维对齐)。
- 工程注意点:① 标准化 (x-mean)/std 依赖广播,先确认 mean/std 的 shape 是列向量;② 广播结果可能瞬间分配巨大数组,大模型训练前先估算 nbytes;③ 混用 Python 循环与广播时注意性能对比;④ 单测覆盖「对齐成功/失败」两类 shape;⑤ pandas 的自动对齐是标签对齐,与 numpy 的 shape 广播不同,避免概念混用。
P79 · 知识点:pandas Series 与 DataFrame
【题目】关于 pandas 的 Series 与 DataFrame,下列说法正确的是?
A. Series 只能存储数值类型,DataFrame 才能存字符串 B. DataFrame 必须连接 SQL 数据库才能创建 C. Series 与 Python list 完全等价,没有任何标签信息 D. Series 是带标签的一维数组,DataFrame 是带行列标签的二维表结构
答案:D
【考点】pandas 两大核心数据结构的维度与标签特性。
【结论】选 D。Series 一维 + index;DataFrame 二维 + index/columns。
【逐项辨析】
- A 错误:二者都可存数值、字符串、时间戳等。
- B 错误:也可从 dict、list、CSV、Excel 等创建。
- C 错误:Series 有 index,list 无标签。
- D 正确:标准定义。
【知识点】 pandas 两大容器:
- Series:一维、带 index 标签的数组,可存数值/字符串/时间戳等;type(df["score"]) 是 Series。
- DataFrame:二维表,= 共享同一 index 的多列 Series,另有 columns;可从 dict、list、CSV/Excel/SQL 等创建。 与 Python list 的差别:list 无标签、可异构;Series/DataFrame 以标签对齐参与运算——两个 index 不同的 Series 相加时,对不齐的位置会变成 NaN,这与 numpy 按位置广播完全不同。与 numpy 的分工:numpy 适合同构数值批量计算;pandas 适合带行列标签的表格清洗、聚合、连接。常用属性:shape、dtypes、index、columns、info()、describe()。df[["score"]](双括号)保持二维 DataFrame,单括号降维成 Series——这是笔试与日常代码的高频坑。底层可转 numpy(to_numpy()/.values),但会丢掉标签信息。
【记忆锚点】「Series 一列一条心,DataFrame 一张表;标签对齐是 pandas 的命。」
【易混对比】
df["col"]得 Series;df[["col"]]得单列 DataFrame。
【自测】 如何创建 DataFrame 并计算 score 列均值?
答:
pd.DataFrame({...})后df["score"].mean()。与 P80/P81 连考。
【知识关联】
- 同库关联:与 P80(loc/iloc)、P81(缺失值)、P76–P78(numpy)构成 pandas 核心题群;与 P23–P32(dict,DataFrame 可视作「列名→Series」的有序映射)对照。数据岗笔试几乎必考 Series/DataFrame 区别与 df["col"] 返回类型。
- 实现层:Series 是一维带 index 的异质可扩展数组(底层常为 numpy 数组 + 索引引擎);DataFrame 是共享同一 index 的多列 Series 字典,另有 columns。创建途径:pd.DataFrame(dict)、pd.read_csv、pd.read_excel、pd.read_sql、from_records 等。df["score"] 降维得到 Series(name="score");df[["score"]] 保持二维。对齐:两个 Series 运算时按 index 标签对齐,缺失处变 NaN——这与 numpy 按位置广播完全不同。属性:shape、dtypes、index、columns、values/to_numpy()。大表可指定 dtype、usecols 降低内存。
- 面试追问:① 何时用 DataFrame、何时用 numpy?② df.dtypes 与 df.info() 看什么?③ 如何把 dict 列表变成 DataFrame? 【拓展延伸】
- 变式问法:① 问 type(df["name"])(Series)与 type(df[["name"]])(DataFrame);② 给出两个 index 不同的 Series 相加问结果;③ 问 pd.Series([1,2,3], index=["a","b","c"]) 如何按标签取值。
- 版本差异:pandas 1.x → 2.x 引入 copy-on-write、更严的 dtype、pyarrow 引擎(dtype_backend="pyarrow")、缺失表示优化;2.2+ 继续清理 deprecated API(如 Series.append);DataFrame.append 已移除,改 pd.concat。核心「标签对齐」哲学未变。
- 工程注意点:① EDA 先 df.shape/df.dtypes/df.isna().sum();② 避免 df["col"] = df["col"].apply(python_loop) 的慢路径,优先向量化;③ 类别列用 category 省内存;④ 管线中固定随机种子与列顺序,保证可复现;⑤ 与 numpy 互转时注意索引丢失与视图问题(P77)。
P80 · 知识点:pandas loc 与 iloc
【题目】对于如下 DataFrame,df.loc[1, "score"] 与 df.iloc[1, 1] 的结果分别是?
import pandas as pd
df = pd.DataFrame(
{"name": ["Tom", "Amy", "Bob"], "score": [80, 90, 70]},
index=[1, 10, 2]
)A. 90 和 90 B. 80 和 80 C. 80 和 90 D. 90 和 80
答案:C
【考点】loc 按标签索引,iloc 按整数位置索引。
【结论】选 C。loc[1,"score"] 取 index 标签为 1 的行(Tom,80);iloc[1,1] 取第 2 行第 2 列(Amy,90)。
【逐项辨析】
- A 错误:把
loc[1]误当成位置。 - B 错误:把
iloc[1]误当成标签。 - C 正确:标签与位置在此 index 下指向不同行。
- D 错误:两者结果对调。
【知识点】 pandas 取数有两套坐标系,本题用自定义 index=[1,10,2] 把差异逼出来:
- loc:按标签。df.loc[1, "score"] 找 index 标签为 1 的行 → Tom → 80。切片 loc[a:b] 包含右端标签。
- iloc:按整数位置(0-based)。df.iloc[1, 1] 取第 2 行第 2 列 → Amy → 90。切片 iloc[a:b] 不含右端。 因此结果是 80 和 90,选 C。陷阱在于:默认 RangeIndex 为 0..n-1 时 loc/iloc「看起来一样」,自定义 index 后立刻分叉。相关 API:at/iat(标量更快);布尔过滤标准写法 df.loc[df.score > 80, "name"]。赋值务必写 df.loc[mask, col] = v,避免 df["col"][mask] = v 触发 SettingWithCopyWarning(可能写到副本上)。记忆:「loc 认标签,iloc 认位置;切片时 loc 含尾,iloc 不含尾。」
【记忆锚点】「loc 认标签,iloc 认位置;切片时 loc 含尾,iloc 不含尾。」
【易混对比】
| 访问方式 | 索引依据 | 切片右端 |
|---|---|---|
| loc | 行列标签 | 包含 |
| iloc | 整数位置 | 不包含 |
【自测】 上题 df.iloc[0:2, 0] 得到什么?
答:
["Tom","Amy"](按位置取前两行 name)。与 P79 连考。
【知识关联】
- 同库关联:与 P79(Series/DataFrame 定义)、P81(缺失值)、P22/P12(切片右开区间——iloc 与之类似,loc 却闭区间)对照;作业/笔试最爱用「自定义 index」区分 loc 与 iloc。
- 实现层:loc 基于 index/columns 标签查找,内部走索引引擎(哈希或有序);iloc 基于 0..n-1 的整数位置,类似 ndarray/Python list。切片:loc[a:b] 包含 b 标签;iloc[a:b] 不含 b。布尔条件:df.loc[df["score"] > 80, "name"] 是标准过滤+选列写法。标量快捷方式:at[row_label, col_label]、iat[r, c] 比 loc/iloc 更快。陷阱:index 为 0,1,2,... 时两者结果常相同,易形成错误直觉;自定义 index=[1,10,2] 后 loc[1] 取标签 1(第一行 Tom),iloc[1] 取位置 1(第二行 Amy)。赋值:df.loc[mask, "col"] = value 是推荐写法;df["col"][mask] = value 可能触发 SettingWithCopyWarning。
- 面试追问:① SettingWithCopyWarning 从哪来?② 如何安全批量赋值?③ df.loc[1] 与 df.iloc[1] 在默认 index 下是否总相同? 【拓展延伸】
- 变式问法:① 给出 index=[1,10,2] 的 DataFrame,比较 loc[1,"score"] 与 iloc[1,1];② 问 df.iloc[0:2, 0] 的结果;③ 问 df.loc[df.score>=80, ["name"]] 返回类型(DataFrame)。
- 版本差异:loc/iloc 语义长期稳定;pandas 2.x copy-on-write 下部分切片赋值行为更安全,但警告机制仍在;df.loc 链式索引旧写法逐步被 assign/query/掩码赋值取代。
- 工程注意点:① 团队规范统一:取行列优先 loc/iloc,避免裸 df[...] 歧义;② 过滤+赋值必须用 df.loc[mask, col] = v;③ 重置 index 用 reset_index(drop=True) 后再依赖位置逻辑,减少标签/位置混用;④ 单测对「自定义 index」做回归;⑤ 大表 .loc 条件列若非唯一索引,性能可观测后再优化(设索引/分类)。
P81 · 知识点:pandas 缺失值处理
【题目】在 pandas 中,删除 DataFrame 中含有缺失值的行,常用方法是?
A. df.dropna() B. df.isnull() C. df.findna() D. df.remove()
答案:A
【考点】pandas 缺失值检测与删除 API。
【结论】选 A。dropna 删除含缺失的行/列;isnull/isna 只做检测。
【逐项辨析】
- A 正确:标准删除 API;可配合
subset=、how=。 - B 错误:
isnull()/isna()返回布尔掩码,不删除。 - C 错误:无
findna方法。 - D 错误:无
remove删除缺失值的方法。
【知识点】 pandas 缺失值处理三板斧:检、删、填。
- 检测:isna()/isnull()(二者是别名)、notna();聚合看 df.isna().sum() 或 .mean()(缺失率)。注意 nan != nan,不能用 == np.nan 判断缺失;对象列的 None 与数值列的 NaN 通常都会被 isna 识别。
- 删除:dropna(axis=0|1, how="any"|"all", subset=[...], thresh=n)。默认 axis=0, how="any":任一列缺失即删行;how="all" 仅当整行全缺才删;subset=["score"] 只考察指定列;thresh=n 要求至少 n 个非缺失。本题问「删除含缺失值的行」,标准答案是 A. df.dropna()。
- 填充:fillna(value|dict|Series)、ffill()/bfill()、interpolate();不能用 df.remove()(无此 API),isnull() 只检测不删除,也没有 findna()。 工程上先分析缺失机制(MCAR/MAR/MNAR),再选删/填/建模指示变量;填充统计量只能在训练集上 fit,避免验证集泄漏;业务「0」与缺失不同,勿盲目 fillna(0)。
【记忆锚点】「检用 isna,删 dropna,补 fillna;notna 是反过来。」
【易混对比】
isnull与isna是别名。- 默认
how="any":任一缺失即删。
【自测】 如何删除 score 缺失的行,并用均值填充 age 缺失?
答:
df.dropna(subset=["score"]);再用df["age"] = df["age"].fillna(df["age"].mean())(或df["age"].fillna(df["age"].mean(), inplace=True))。注意别写成df.fillna(df["age"].mean())——那是对整个 DataFrame 调 fillna,会把 age 的均值灌进所有含缺失的列:pandas 3.0.3 实测df.fillna(df["age"].mean())会把name、score列的缺失也一起填成 35.0。与 P79 连考。
【知识关联】
- 同库关联:与 P79(DataFrame)、P80(loc 过滤)、P61–P62(异常与数据合法性)衔接;特征工程第一步通常是缺失值分析。与 Excel/SQL 的「删除空行/COALESCE」概念对照。
- 实现层:pandas 用 NaN(float)/None/pd.NA 表示缺失;isna() 逐元素返回布尔 DataFrame/Series。dropna(axis=0|1, how="any"|"all", subset=[...], inplace=..., thresh=n):默认 axis=0, how="any",任一列缺失即删行。fillna(value|dict|Series) 填充;ffill/bfill 前后向;interpolate 插值。nan != nan,故 df["c"] == np.nan 几乎永远全 False,必须用 isna。对象列中的 None 与数值列中的 NaN 检测结果通常都为 True。thresh=n 表示至少 n 个非缺失才保留。统计:df.isna().sum()、df.isna().mean() 看缺失率。
- 面试追问:① 缺失机制 MCAR/MAR/MNAR 如何影响策略?② 为何不能随意删行?③ 训练集填充统计量泄漏到验证集会怎样? 【拓展延伸】
- 变式问法:① 问 how="all" 与 how="any" 差异;② 问 subset=["score"] 含义;③ 给出混合 None/NaN 表问 isna().sum()。
- 版本差异:pandas 2.x 推荐 pd.NA 与 nullable dtype(Int64、boolean);弱化 fillna(method=...)(改用 ffill()/bfill());pyarrow 后端对缺失与字符串处理更高效;dropna/isna 核心语义稳定。
- 工程注意点:① 先画缺失率与缺失相关性,再选删/填/建模指示变量;② 填充统计量必须只在训练集 fit;③ 业务上「0」与缺失不同,勿盲目 fillna(0);④ 管线封装 SimpleImputer(sklearn)或自定义 Transformer,保证可复现;⑤ 导出前检查关键列零缺失(数据契约)。
P82 · 知识点:re.match 与 re.search
【题目】执行下列代码后,m1 和 m2 分别是?
import re
s = "abc123"
m1 = re.match(r"\d+", s)
m2 = re.search(r"\d+", s)A. 二者都匹配到 "123" B. m1 匹配 "123",m2 为 None C. m1 为 None,m2 匹配 "123" D. 二者都为 None
答案:C
【考点】re.match 只从字符串开头匹配,re.search 扫描整个字符串。
【结论】选 C。开头是字母故 match 失败;search 能找到 123。
【逐项辨析】
- A 错误:match 不能跳过开头非数字部分。
- B 错误:二者作用说反了。
- C 正确:match 锚定开头;search 全串搜索。
- D 错误:search 可以匹配到
123。
【知识点】 re 模块四个入口方法决定「从哪开始匹配、要几处」:
| 方法 | 匹配范围 | 返回 |
|---|---|---|
| re.match | 仅字符串起始位置 | Match 或 None |
| re.search | 扫描整个字符串的第一处 | Match 或 None |
| re.findall | 所有匹配 | list |
| re.fullmatch | 必须覆盖整个字符串 | Match 或 None |
| 本题 s="abc123",模式 r"\d+":开头是字母,match 在位置 0 失败 → m1 is None;search 扫描到 "123" → m2 匹配成功。故选 C。 | ||
| 注意:match 不等于 fullmatch——match 只锚定开头,不要求匹配到结尾;要整串匹配用 fullmatch(或在模式中加 $)。Match 对象常用 group()/group(1)/span()。多次使用同一模式时 re.compile 得到 Pattern 对象可复用。flags:re.I 忽略大小写、re.M 多行(^/$ 变成行首行尾)、re.S 让 . 吃换行。底层多为回溯型 NFA,灾难性回溯要靠模式设计规避。 |
【记忆锚点】「match 只看开头,search 全场找;findall 收全套,fullmatch 必须全对上。」
【易混对比】
- match 不等于“必须匹配到结尾”;要全串匹配用 fullmatch。
【自测】re.findall(r"[A-Z]+", "aBcDEFg") 与 re.fullmatch(r"[A-Z]+", "aBcDEFg") 结果?
答:
['B','DEF'];fullmatch 为 None。与 P83 连考。
【知识关联】
- 同库关联:与 P83(findall 与分组)、P33–P40(字符串处理)构成文本题群;日志清洗、表单校验、爬虫提取都依赖正则入口选择。与 Java String.matches(整串匹配)对照:Python match 只锚定开头,不是 fullmatch。
- 实现层:re.match(pattern, string) 仅尝试从起始位置匹配,失败即 None;re.search 从左到右扫描,返回第一处成功匹配;re.findall 返回全部;re.fullmatch 要求覆盖整个字符串。匹配成功得 Match 对象:group()/group(0) 整段,group(1) 分组,span() 位置。re.compile 返回 Pattern,多次使用可复用,性能与可读性更好。默认贪婪;*?/+? 非贪婪;^/$ 在 re.M 多行模式下匹配每行首尾,在默认模式只匹配字符串首尾。底层多为回溯型 NFA,极端模式可能指数回溯。
- 面试追问:① 贪婪与非贪婪区别?② 如何避免回溯爆炸?③ match 与 fullmatch 差异? 【拓展延伸】
- 变式问法:① re.findall(r"[A-Z]+", "aBcDEFg")(['B','DEF'])与 fullmatch 结果(None);② re.match(r"\d+", "abc123") vs re.search;③ 问 re.match(r"a|ab", "ab").group()(分支顺序影响,左侧优先常得 a)。
- 版本差异:正则语法多年稳定;Python 3.11+ 部分错误信息改进;regex 第三方模块支持更丰富特性(变长 lookbehind 等);标准库 re 无「拥有所有格」量词,需靠设计模式规避灾难性回溯。
- 工程注意点:① 锚定开头用 match/fullmatch,不要误以为 match 会扫全串;② 复杂解析(HTML/JSON/带嵌套)用专用解析器,不要硬上正则;③ 预编译常用 Pattern 到模块级常量;④ 正则要带单元测试与边界用例;⑤ 日志字段提取优先结构化日志,而不是事后正则考古。
P83 · 知识点:正则 findall 与分组
【题目】执行 re.findall(r"(\w+)@(\w+)", "u1@dom u2@site") 的结果是?
A. ["u1", "dom", "u2", "site"] B. [("u1@dom",), ("u2@site",)] C. ["u1@dom", "u2@site"] D. [("u1", "dom"), ("u2", "site")]
答案:D
【考点】findall 在模式含有分组时返回分组元组列表。
【结论】选 D。模式中有两组 (...),每条匹配返回一个二元组。
【逐项辨析】
- A 错误:不会把所有组拍平成单一列表。
- B 错误:元组内容应是捕获组,不是整串再套一层。
- C 错误:这是无分组时 findall 的返回形式。
- D 正确:多分组 → list of tuples。
【知识点】 re.findall 的返回类型完全由模式里有无捕获组、有几个捕获组决定,这是本题考点:
- 无捕获组:返回 list[str],元素是整段匹配。例如 r"\w+@\w+" → ['u1@dom','u2@site']。
- 恰好一个捕获组:返回 list[str],元素是该组内容(不是元组)。
- 多个捕获组:返回 list[tuple]。本题 r"(\w+)@(\w+)" 有两组 → [('u1','dom'),('u2','site')],选 D。
- 非捕获组 (?:...):参与匹配但不进入返回值,例如 r"(?:\w+)@(\w+)" → ['dom','site'](只要域名时常用)。
- 命名分组 (?P<name>...):在 findall 里命名分组与普通捕获组完全同等计数——只有 1 个组(命名的也一样)时返回 list[str],≥2 个组才返回 list[tuple]。本机 3.12.3 实测:
re.findall(r"(?P<y>\d{4})", "2024-01 1999-12")→['2024', '1999'](list[str],不是元组);re.findall(r"(?P<y>\d{4})-(?P<m>\d{2})", …)→[('2024', '01'), ('1999', '12')]。命名只是给 match 对象加访问方式:group("name")/groupdict()。 若想只要域名,用 r"\w+@(\w+)"。注意 \w 默认是 Unicode 字母数字下划线,不是严格 ASCII。需要更完整信息(位置、所有组)时用 finditer 拿 Match 迭代器。
【记忆锚点】「findall 有括号就吐括号里的;要整串就别加捕获括号。」
【易混对比】
| 模式 | findall 返回 |
|---|---|
r"\w+@\w+" | ['u1@dom','u2@site'] |
r"(\w+)@(\w+)" | [('u1','dom'),('u2','site')] |
r"(?:\w+)@(\w+)" | ['dom','site'] |
r"(?P<user>\w+)@\w+"(仅 1 个命名组) | ['u1','u2']——不是元组列表 |
r"(?P<user>\w+)@(?P<host>\w+)"(2 个命名组) | [('u1','dom'),('u2','site')] |
【自测】 如何只返回域名 ["dom","site"]?
答:
r"\w+@(\w+)"或r"(?:\w+)@(\w+)"。与 P82 连考。
【知识关联】
- 同库关联:与 P82(match/search)、P23–P24(dict,可把 findall 结果转成字典列表)衔接;爬虫邮箱/手机号提取、日志 kv 解析是典型应用。Java Matcher.group 与 Python 分组概念可对照。
- 实现层:re.findall 返回类型完全由模式中的捕获组决定:① 无组 → list[str],元素是整段匹配;② 恰一个捕获组 → list[str],元素是该组内容(不是元组);③ 多个捕获组 → list[tuple[str,...]];④ 非捕获组 (?:...) 不出现在结果中;⑤ 命名分组 (?P<name>...) 在 findall 中与普通捕获组同等计数:恰 1 个组(含命名)→ list[str],≥2 个组 → list[tuple[str,...]](实测
re.findall(r"(?P<y>\d{4})", …)得['2024','1999']);命名的价值在 match 对象上用group("name")/groupdict()取值。finditer 返回 Match 迭代器,信息更全。\w 默认 Unicode 字母数字下划线,不是严格 ASCII。可选 flags:re.I、re.M、re.S(. 匹配换行)。 - 面试追问:① 非捕获组意义?(参与匹配但不占用返回/编号)② 如何只提取域名?③ 分组编号与嵌套规则? 【拓展延伸】
- 变式问法:① r"\w+@(\w+)" → ['dom','site'];② r"(?:\w+)@(\w+)" 同上;③ 问可选分组在 findall 中可能出现空串的细节(需实测记忆)。
- 版本差异:findall 分组规则稳定;Python 3.7+ 正则与 f-string 之外的文本处理路径不变;regex 模块支持更多捕获返回形式;类型标注可用 list[tuple[str,str]] 表达结果。
- 工程注意点:① 写 findall 前先明确「要不要整段」,再决定是否加捕获括号;② 结果转 DataFrame 时列名与分组顺序一致;③ 对用户输入的正则注意 ReDoS;④ 单测覆盖空匹配、重叠、大小写;⑤ 提取失败要有兜底与监控,避免静默丢数据。
P84 · 知识点:生成器 send 与 yield from
【题目】关于生成器的 send() 与 yield from,下列说法正确的是?
A. gen.send(value) 中的 value 会作为上一次 yield 表达式的返回值传回生成器 B. yield from subgen 只是语法糖,不能把子生成器的返回值带给外层 C. 生成器第一次启动不能使用 next(gen) 或 send(None) D. yield from 会开启新线程执行子生成器
答案:A
【考点】生成器双向通信与 yield from 委托语义。
【结论】选 A。send(v) 将 v 作为当前挂起点 yield 表达式的结果注入生成器。
【逐项辨析】
- A 正确:例如
x = yield中,gen.send(5)使x得到 5。 - B 错误:
yield from会把子生成器return的值作为整个表达式的结果(PEP 380)。 - C 错误:第一次必须
next(gen)或send(None),二者等价。 - D 错误:仍在当前线程协作式执行,无线程创建。
【知识点】
def echo():
while True:
received = yield
print("recv:", received)
g = echo()
next(g) # 启动
g.send("hi") # received = "hi"yield from 还会转发 send/throw/close,并捕获子生成器 return 值。
【记忆锚点】「先 next 再 send;send 把值塞回 yield 的左边;yield from 全权委托。」
【易混对比】
yield产出值 vs yield 表达式接收 send 值。yield fromvs 手写for x in sub: yield x:后者拿不到 return 值。
【自测】
def g():
x = yield 1
y = yield x + 1
return y * 2
it = g()
print(next(it))
print(it.send(10))
print(it.send(20))输出依次是什么?
答:打印 1、11;最后一次 send 触发
StopIteration: 40。与 P73 连考。
【知识关联】
- 同库关联:与 P73–P75(迭代器/生成器/GIL)构成生成器题群;yield from 是 asyncio 时代之前的委托基础,也与装饰器(P74)一起出现在高级语法考题。数据管道中用生成器惰性处理,可大幅省内存。
- 实现层:生成器函数调用后返回生成器对象,首次必须 next(g) 或 g.send(None) 启动至第一个 yield(否则 TypeError: can't send non-None value to a just-started generator)。g.send(v) 恢复执行,并把 v 赋给挂起点的 yield 表达式结果。yield from subgen(PEP 380)等价于打开子生成器并双向转发:send/throw/close 透传,子生成器 return value 的 value 成为 yield from 表达式的值(以 StopIteration.value 形式传出)。手写 for x in sub: yield x 拿不到 return 值,也不转发 send。生成器耗尽后再 send/next 抛 StopIteration。
- 面试追问:① 生成器与迭代器关系?② yield from 在 asyncio 中的角色?③ 为何第一次不能 send 非 None? 【拓展延伸】
- 变式问法:① 预测 next/send 链输出与最终 StopIteration 值;② 问 yield from 能否替换为 for-yield(拿不到 return 时不能等价);③ 问生成器表达式是否支持 send(是生成器,支持,但通常只用 next)。
- 版本差异:yield from 自 Python 3.3;3.4+ asyncio 基于生成器协程;3.5+ async/await 成为一等语法,官方更推荐原生协程而不是 yield from 写协程;生成器本身仍是惰性序列与管道的核心工具。
- 工程注意点:① ETL 用生成器链避免一次读入全表;② 库 API 若既支持 send 又被用户 next,文档写清启动约定;③ 异步业务用 asyncio 而不是手搓 yield 协程;④ 注意生成器未关闭导致资源句柄泄漏(.close()/contextlib);⑤ 单测用 list(gen) 消费并断言元素与 StopIteration.value。
P85 · 知识点:模块导入与 name
【题目】关于 Python 模块导入,下列说法正确的是?
A. from mod import func 会把 mod 中所有公开名字导入当前命名空间 B. if __name__ == "__main__": 块中的代码,在模块被 import 时默认不会执行,作为脚本直接运行时会执行 C. import mod 之后,不可能把 func 直接绑定到当前命名空间 D. 同一模块在一个进程中无论怎么 import,文件都会被反复执行多次
答案:B
【考点】导入机制、sys.modules 缓存与主模块判断。
【结论】选 B。被 import 时 __name__ 是模块名;直接运行时是 "__main__"。
【逐项辨析】
- A 错误:只导入
func;导入全部是from mod import *(不推荐)。 - B 正确:可复用又可运行的标准写法。
- C 错误:
from mod import func之后可直接func()。 - D 错误:
sys.modules缓存,再次 import 不会重新执行顶层代码。
【知识点】
| 写法 | 使用方式 |
|---|---|
import mod | mod.func() |
from mod import func | func() |
import mod as m | m.func() |
from mod import * | 易冲突,慎用 |
搜索路径 sys.path;包内相对导入 from . import x。
【记忆锚点】「import 拿模块,from 拿名字;name 判主程序,sys.modules 防重载。」
【易混对比】
if __name__ == "__main__"是入口守卫,不是性能优化。
【自测】 模块有顶层 print("loaded") 与 main 守卫 print("main")。先 import 再直接运行,分别打印什么?
答:import 只打印 loaded;直接运行打印 loaded 与 main。与 P82 连考。
【知识关联】
- 同库关联:与 P41–P52(函数与作用域:模块顶层即全局作用域)、P53(类的
__init__构造方法;注意本库没有「包与__init__.py」专题题)、P73–P74(生成器/装饰器常写在模块中,import 时执行装饰)构成工程结构题群。与 Java 的 package/静态初始化对照:Python 模块顶层代码在首次 import 时立即执行。 - 实现层:import mod:解释器按 sys.path 查找 mod,若 sys.modules 已有则直接取缓存,否则创建模块对象、执行顶层代码、写入 sys.modules["mod"],当前命名空间绑定名字 mod。from mod import func:同样执行模块,但把 func 绑定到当前命名空间。from mod import *:导入公有名(受 all 控制),易冲突、不利静态分析。包:目录 + init.py(3.3+ 命名空间包可省略);相对导入 from . import x 只能在包内使用。name:被导入时为模块名,作为脚本运行时为 "main"。循环导入:A import B、B import A 时,若访问尚未初始化的名字会 ImportError/AttributeError。
- 面试追问:① 循环导入如何产生与如何解?② 为何避免 import *?③ sys.modules 与 importlib.reload 区别? 【拓展延伸】
- 变式问法:① 模块有顶层 print("loaded") 与 main 守卫 print("main"),先 import 再直接运行分别输出什么;② 问 if name == "main" 在 pytest/被导入时是否执行;③ 问 import a.b 与 from a import b 的绑定差异。
- 版本差异:3.3+ 命名空间包;3.4+ importlib 与显式相对导入成为推荐;3.5+ 异步可运行模块内定义 async;3.8+ importlib.metadata;pyproject/pip/PEP 517 后打包与入口点(console_scripts)更常见,main.py 支持 python -m pkg。
- 工程注意点:① 可执行脚本一律加 main 守卫,保证可被测试导入;② 避免 import 副作用(连接网络、起线程);③ 包结构清晰,业务避免深相对导入地狱;④ 循环依赖通过提取接口模块、依赖注入或函数内延迟导入解决;⑤ 打包时用 all/显式导出,保证公共 API 稳定。