Skip to content

九、迭代器/生成器/装饰器/GIL 与 numpy/pandas/正则/模块(P73–P85) ​

P73 · 知识点:生成器与 yield ​

【题目】yield 关键字的作用是? A. 暂停函数执行并返回一个值,下次调用从暂停处继续 B. 结束函数 C. 抛出异常 D. 开启新线程

答案:A

【考点】生成器机制:yield 保存执行状态,惰性求值。

【结论】选 A。yield 关键字可以使生成器函数暂停执行并返回一个值,下次调用时从暂停处继续执行,实现惰性求值。

【推导过程】

python
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 的函数)或生成器表达式创建。其核心机制包括:

  1. 惰性求值(Lazy Evaluation):生成器不会一次性把所有结果加载到内存,而是按需逐个产生值,非常适合处理大规模数据流或无限序列。
  2. 状态保存:每次 yield 时,Python 解释器会保存函数的局部变量、指令指针等执行状态,封装在生成器对象中;下次调用 __next__() 时恢复现场继续执行。
  3. 生成器对象:调用生成器函数得到的是生成器对象,它实现了迭代器协议(__iter__ 和 __next__),因此可直接用于 for 循环或 next() 函数。
  4. 双向通信:Python 的 yield 不仅是输出通道,通过 send(value) 方法还可向生成器内部发送数据,实现协程(Coroutine)效果。例如:
python
   def accumulator():
       total = 0
       while True:
           val = yield total
           if val is None:
               break
           total += val
  1. 内存优势:与列表推导式相比,生成器表达式 (x for x in range(10**8)) 几乎不占用额外内存,而列表推导式 [x for x in range(10**8)] 会瞬间消耗大量 RAM。

【记忆锚点】 “yield 一停一等,省内存、惰性行;暂停处接着跑,生成器是宝。”

【易混对比】

对比维度yieldreturn生成器列表
执行行为暂停并保存状态终止函数并返回惰性求值,逐项产出一次性全部创建
内存占用极低(状态机)无(单次返回)低(O(1) 额外空间)高(O(n) 空间)
可否多次返回可以(多次 yield)一次(函数结束)可以(无限序列)已固定长度
适用场景大数据流、无限序列普通函数结果返回遍历大文件、流水线处理需要索引访问的小数据

换问法:若题目改为“以下哪个关键字可以将普通函数转换为生成器函数?”,答案仍是 yield;若改为“生成器与列表推导式的核心区别是什么?”,则应答“惰性求值与内存占用”。

【自测】 以下代码的输出结果是什么?

python
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)。

【推导过程】

python
# 装饰器语法糖
@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)的核心语法特性,其底层依赖以下机制:

  1. 高阶函数:Python 中函数是一等公民(First-Class Citizen),可以像普通变量一样被传递、赋值和返回。装饰器正是利用这一特性,接收函数并返回函数。
  2. 闭包(Closure):装饰器内部通常定义嵌套函数(wrapper),该嵌套函数引用了外部函数的局部变量(被装饰的函数对象),从而形成闭包,确保被装饰函数在后续调用时能够正确执行增强逻辑。
  3. 语法糖解析:Python 解释器在编译阶段遇到 @decorator 时,会将其解析为函数定义后的重新绑定。若存在多层装饰器,如:
python
   @a
   @b
   def f(): ...

则等价于 f = a(b(f)),执行顺序为自下而上。 4. functools.wraps:由于装饰器返回的新函数会丢失原函数的元数据(如 __name__、__doc__),标准库提供 functools.wraps 装饰器来拷贝这些属性,保证内省(Introspection)正确性。 5. 带参数装饰器:当装饰器本身需要配置参数时,通常采用三层嵌套结构——外层函数接收参数并返回真正的装饰器,中层装饰器接收被装饰函数,内层 wrapper 接收实际调用参数:

python
   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") 的输出。

python
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 架构中最具争议的设计之一,深入理解其原理与影响至关重要:

  1. 设计动机:CPython 使用引用计数(Reference Counting)进行内存管理,对象的生命周期由 ob_refcnt 字段决定。若没有 GIL,多线程并发地修改引用计数将导致竞态条件与内存泄漏/重复释放。GIL 以粗粒度锁的方式简化了 C 扩展开发,避免了为每个对象加细粒度锁的巨大开销。
  2. 释放时机: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),与本题【拓展延伸】的版本差异一致,不要再按"条指令"表述。
  3. CPU 密集型瓶颈:对于计算密集型任务(如数值计算、图像处理),由于 GIL 的存在,多线程无法并行,此时应使用 multiprocessing 模块创建独立进程,每个进程拥有独立的 Python 解释器与 GIL,从而利用多核。
  4. 替代方案:
    • 多进程(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%。
  5. 适用场景选型:IO 密集型首选多线程或 asyncio;CPU 密集型首选多进程或 C 扩展;高并发网络服务首选 asyncio。

【记忆锚点】 “CPython 有把 GIL 锁,CPU 并行靠进程过;IO 等待会解锁,多线程在此时显卓。”

【易混对比】

对比维度GILthreading.Lockmultiprocessing
作用范围整个解释器(全局)用户代码中的特定代码块独立进程,独立解释器
保护对象字节码执行 + 引用计数用户共享资源进程地址空间隔离
多核利用无法利用(CPU 密集)无法突破 GIL 限制可以完全利用多核
适用场景CPython 内部机制线程间共享数据保护CPU 密集型计算
开销解释器内部自动管理轻量级(用户显式加锁)较重(进程创建与 IPC)

换问法:若题目改为“在 CPython 中,如何使 CPU 密集型任务利用多核并行?”,答案应为“使用 multiprocessing 模块或编写释放 GIL 的 C 扩展”;若改为“Python 3.13 提供了什么实验性特性来移除 GIL?”,则应答“无 GIL 构建(--disable-gil)”。

【自测】 以下代码运行在多核 CPU 的 CPython 环境中,执行 cpu_bound() 时能否利用多核?如何改进?

python
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 的值是?

python
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 的内容是?

python
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 切片像照片。」

【易混对比】

操作numpyPython 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 的结果是?

python
import numpy as np
a = np.array([[1], [2], [3]])   # shape (3, 1)
b = np.array([10, 20])          # shape (2,)
c = a + b

A. 报错,形状不匹配无法相加 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;
  2. 每一维要求长度相等,或其中一方为 1,否则抛 ValueError: operands could not be broadcast together;
  3. 长度为 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] 的结果分别是?

python
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,保证可复现;⑤ 导出前检查关键列零缺失(数据契约)。

【题目】执行下列代码后,m1 和 m2 分别是?

python
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 的返回类型完全由模式里有无捕获组、有几个捕获组决定,这是本题考点:

  1. 无捕获组:返回 list[str],元素是整段匹配。例如 r"\w+@\w+" → ['u1@dom','u2@site']。
  2. 恰好一个捕获组:返回 list[str],元素是该组内容(不是元组)。
  3. 多个捕获组:返回 list[tuple]。本题 r"(\w+)@(\w+)" 有两组 → [('u1','dom'),('u2','site')],选 D。
  4. 非捕获组 (?:...):参与匹配但不进入返回值,例如 r"(?:\w+)@(\w+)" → ['dom','site'](只要域名时常用)。
  5. 命名分组 (?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 错误:仍在当前线程协作式执行,无线程创建。

【知识点】

python
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 from vs 手写 for x in sub: yield x:后者拿不到 return 值。

【自测】

python
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 modmod.func()
from mod import funcfunc()
import mod as mm.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 稳定。

持续学习,持续积累。