Interview questions preparation(1)

Last updated on September 24, 2025 am

一些面试的问题,问题来源多种多样,一些问题的回答采用了AI的回复,后期可能重新组织文章结构

unordered_map和dict的底层实现是什么

unordered_map(C++)和dict(Python)都是编程中常用的关联数组(或叫字典、映射)实现,它们底层都基于哈希表(Hash Table),但在具体实现细节上有所不同。下面我将用一个表格对比它们的核心特征,然后详细解释它们的底层实现。

特性 C++ unordered_map Python dict
底层结构 哈希表 (Hash Table) 哈希表 (Hash Table)
冲突解决 开链法 (Separate Chaining),桶内超限可能转红黑树 开放寻址法 (Open Addressing),伪随机探测
哈希函数 使用 std::hash 或自定义函数对象 使用内置 hash() 函数
内存管理与扩容 需手动设置或依赖默认的桶数和最大负载因子 自动扩容,无需手动干预
键的可哈希要求 键类型需支持哈希计算和 == 比较 键必须是不可变且可哈希的类型
有序性 无序 Python 3.7保持插入顺序

🔍 底层实现原理

C++ unordered_map

unordered_map 的底层是一个动态数组,数组的每个元素是一个“桶”(bucket),桶背后连接着一个链表(或小型容器)。当你插入一个键值对时: 1. 计算哈希值:首先对键(key)应用哈希函数(默认是 std::hash 的特化版本),得到一个哈希值(一个大的整数)。 2. 确定桶索引:将这个哈希值对当前桶数组的大小(bucket_count)取模,从而确定该键值对应放入哪个桶中。 3. 处理冲突:如果多个键映射到同一个桶(哈希冲突),unordered_map 采用开链法(Separate Chaining),将这些发生冲突的键值以链表形式存储在同一个桶内。在某些实现(如某些标准库)中,当单个桶内的元素数量超过一定阈值(例如8个),为了提升查询效率,可能会将链表转换为红黑树。 4. 动态扩容(Rehashing):哈希表有一个负载因子(load factor),即 元素数量 / 桶的数量。当负载因子超过预设的最大值(默认通常是 1.0)时,为了保持操作的高效性,容器会自动进行扩容。这会创建一个新的、更大的桶数组,并将所有已有的键值对重新哈希(rehash)到新的桶数组中。扩容是一个相对耗时的操作。

Python dict

Python 的 dict 同样基于哈希表,但具体实现细节有所不同: 1. 计算哈希值:对键使用内置的 hash() 函数得到哈希值。 2. 确定索引:利用哈希值的某些位计算出一个索引值(在 CPython 实现中,这个过程会考虑当前哈希表的大小)。 3. 处理冲突:Python 使用的是开放寻址法(Open Addressing)中的伪随机探测(pseudo-random probing)。当发生冲突时,它会根据一个特定的探测序列(pseudo-random probe sequence)在数组中寻找下一个可用的空位,而不是像开链法那样使用链表。 4. 动态扩容:Python 字典会自动扩容。当哈希表的拥挤程度(大致可理解为剩余空间比例)低于某个阈值时,解释器会分配一个更大的内存空间,并重新插入所有键值对。

⚠️ 注意事项与特点

  • 键的类型:无论是 unordered_map 还是 dict键必须是可哈希的。在 C++ 中,这意味着自定义类型需要提供哈希函数和等值比较函数。在 Python 中,键必须是不可变类型(如整数、浮点数、字符串、元组等),因为可变对象(如列表、字典、集合)的哈希值可能会变,这会破坏哈希表的完整性。
  • 性能:在平均情况下,两者插入、删除、查找操作的时间复杂度都是 O(1)。但在最坏情况下(例如所有键都发生哈希冲突),性能会下降。
    • 对于 unordered_map,最坏情况复杂度可能达到 O(n),如果许多键都哈希到同一个桶,导致链表很长(或者树变得复杂)。
    • 对于 dict,在极端冲突下,开放寻址法的查找时间也会增加。
  • 内存使用unordered_map 的开链法每个元素会有额外的指针开销(指向链表下一个节点或树结构)。dict 的开放寻址法通常更节省内存,并且由于其数据存储在连续数组中,缓存局部性(Cache Locality)更好,这在许多情况下能带来实际的速度提升。
  • 顺序:C++ unordered_map不保证任何特定的遍历顺序,并且顺序可能会随着扩容重新哈希而改变。Python 的 dict 自 3.7 版本起官方保证遍历顺序即为元素的插入顺序。

💡 如何选择和使用

  • C++:当需要快速的键值查找且不关心顺序时,优先选择 unordered_map 而不是 map(基于红黑树,有序,操作复杂度为 O(log n))。如果需要有序遍历,则使用 map
  • Python:直接使用 dict 即可。它的实现在大多数情况下都非常高效,并且保持了插入顺序,非常实用。

希望这些解释能帮助你更深入地理解 unordered_mapdict。如果你有其他问题,欢迎继续提出。


pandas和numpy的底层和一些函数的底层实现

Pandas 和 NumPy 是 Python 数据科学的核心,虽然常协同工作,但设计目标和底层实现各有侧重。下面这张表格汇总了它们的核心差异,帮你快速把握全貌。

特性维度 NumPy Pandas
核心数据结构 ndarray (N维数组) Series, DataFrame (带标签的表格结构)
设计目标 高效的数值计算 灵活的数据处理与分析
数据同质性 同质:数组内元素类型必须一致 异构:DataFrame 的不同列可以是不同类型
索引机制 主要为整数索引 支持灵活的标签索引、多层索引和布尔索引
缺失值处理 较基础 原生支持 NaN,并提供丰富的处理方法
内存布局 单一连续内存块 列存储(每列一个独立数组),附加索引结构
性能侧重 大规模数值运算(如矩阵乘法、傅里叶变换) 数据清洗、转换、聚合、关系型操作
底层语言 大量 C/C++ 和 Cython 实现 构建于 NumPy 之上,大量使用 C/C++ 和 Cython 实现

🧠 接下来,我们深入看看它们的一些核心底层机制。

🔧 NumPy 的底层实现

NumPy 的核心是 ndarray(N-dimensional array),它包含以下关键属性[citation:7]: * 数据缓冲区(Data Buffer):一个连续的内存块,存储所有数组元素。这种连续的内存布局是现代 CPU 高效访问数据的基石。 * 形状(Shape):描述数组维度的元组。 * 数据类型(Dtype):指定数组中元素的数据类型(如 int64, float32),确保了数据的同质性。 * 步幅(Strides):一个元组,表示遍历数组每个维度时需要在内存中步进的字节数。这使得数组可以以不同视角查看数据而无需复制。

NumPy 的高性能很大程度上源于向量化操作。这些操作通过 通用函数(ufuncs) 实现,其底层是 C/C++ 代码,避免了 Python 循环的开销,并充分利用了现代 CPU 的 SIMD(单指令多数据流)指令集进行并行计算。

广播机制(Broadcasting) 是 NumPy 的另一个核心特性。它允许不同形状的数组进行算术运算。其规则是:从维度最右边开始对齐,维度相等或其中一方为 1 时即可广播。NumPy 会自动将较小的数组“拉伸”以匹配较大数组的形状,从而无需显式复制数据。

1
2
3
4
5
6
7
8
9
10
import numpy as np

# 向量化操作示例:整个数组相加
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = a + b # 在C层执行高效循环,而非Python层

# 广播机制示例:标量与数组相乘
arr = np.array([[1, 2, 3], [4, 5, 6]])
result = arr * 2 # 标量值2被广播到与arr相同的形状

🗃️ Pandas 的底层实现

Pandas 的 SeriesDataFrame 构建于 NumPy 之上。Series 可看作一个带索引的一维 NumPy 数组,而 DataFrame 是一个由多个 Series(列)组成的二维表格,这些列共享相同的索引

Pandas 采用列式存储。每个 DataFrame 的每一列都是一个独立的内存连续的 NumPy 数组(或类似结构)。这种布局对于按列进行的聚合和计算非常高效,但按行操作可能较慢。

Pandas 的索引引擎是其高效数据查询的关键。索引(如 RangeIndex, Int64Index, CategoricalIndex, DatetimeIndex)一旦创建通常是不可变的,类似于字典的键。它使用哈希表(对于无序索引)或二分查找(对于有序索引)来快速映射标签到数据位置,使得基于标签的查询(如 df.loc[label])非常高效。

1
2
3
4
5
6
7
8
9
10
import pandas as pd

# 创建一个简单的DataFrame
df = pd.DataFrame({
'age': [25, 30, 35],
'salary': [50000, 60000, 70000]
}, index=['Alice', 'Bob', 'Charlie'])

# .loc[] 使用索引引擎进行快速标签查找
alice_salary = df.loc['Alice', 'salary']

Pandas 2.0 引入了对 Apache Arrow 的支持作为另一种后端。Arrow 提供了更高效的内存布局和跨语言能力,尤其在处理字符串和非数值数据时能提升性能并减少内存占用。

⚙️ 核心函数的底层实现

许多高性能操作的背后是精心优化的底层代码。

  • NumPy 的 np.dot() (矩阵乘法):通常会调用底层高度优化的 BLAS (Basic Linear Algebra Subprograms) 库(如 OpenBLAS, Intel MKL)的实现。这些库针对特定 CPU 架构进行了极致优化。
  • Pandas 的 groupby().mean():虽然表面是单行代码,底层可能涉及:
    1. 根据键对行进行哈希或排序,形成分组。
    2. 对每个分组的数值列调用相应的 NumPy 聚合函数(如 np.mean)。
    3. 将结果组装成新的 DataFrame。 整个流程在 Cython/C 层实现,避免了 Python 循环。

🤝 高效协作之道

NumPy 和 Pandas 协同工作,形成了高效的数据处理流程:

  1. 使用 Pandas 进行数据导入和清洗:利用其强大的 IO 工具(如 read_csv)和数据处理功能(处理缺失值、字符串操作、时间序列解析等)。
  2. 转换为 NumPy 数组进行核心数值计算:在需要执行复杂数学运算、机器学习模型训练时,通过 .values.to_numpy() 将 Pandas 数据转换为 NumPy 数组,以发挥其数值计算极限性能。
  3. (可选) 结果转回 Pandas 进行展示或输出:将计算结果重新包装回 DataFrame,便于后续的标签化操作、可视化或导出。
1
2
3
4
5
6
7
8
9
10
11
12
# 典型工作流:Pandas 用于数据准备,NumPy 用于计算
df = pd.read_csv('data.csv') # Pandas 读取数据
df_cleaned = df.dropna() # Pandas 清洗数据

# 提取数值列转换为 NumPy 数组进行高强度计算
matrix = df_cleaned[['feature1', 'feature2']].to_numpy()
# 使用 NumPy 进行奇异值分解 (SVD)
u, s, vh = np.linalg.svd(matrix)

# 将结果转回 Pandas 进行展示
result_df = pd.DataFrame(u, index=df_cleaned.index)
print(result_df.head())

💡 性能优化技巧

  • 优先使用向量化操作:无论在 NumPy 还是 Pandas 中,都应尽量避免显式的 Python 循环,充分利用其内置的、基于 C 的向量化方法。
  • 关注数据类型:在 Pandas 中,使用合适的数据类型能显著减少内存占用并提升速度。例如:
    • pd.to_numeric(..., downcast='integer') 向下转换整数类型。
    • 对低基数(重复值多)的字符串列使用 astype('category')
    • 使用 pd.to_datetime 转换日期时间。
  • 警惕复制操作:理解 NumPy 的视图(view)与副本(copy)以及 Pandas 的 copy=False 等选项,避免不必要的内存复制。

理解 Pandas 和 NumPy 的底层机制,能帮助你在数据处理任务中选择正确的工具和方法,从而编写出更高效、更优雅的代码。


fork()和spawn()分别是什么,以及对应的内存

理解 fork()spawn() 这两种创建子进程的方式及其内存管理机制,对于编写高效、稳定的程序很重要。下面我将为你解释它们的特点、内存处理方式以及如何选择。

📊 核心特性对比

特性维度 fork() spawn() (或 posix_spawn())
创建机制 复制父进程状态 启动新程序
内存管理 写时复制 (Copy-On-Write) 全新加载
执行速度 快(内存延迟复制) 慢(需加载新程序)
资源继承 继承父进程大部分资源(文件描述符、内存状态等) 不继承父进程资源(或仅继承必要部分)
安全性 相对较低(继承父进程状态可能带来风险) 较高(进程独立,起点干净)
平台支持 主要类 Unix 系统(Linux, macOS) 跨平台(包括 Windows)
适用场景 需要快速创建类似父进程的子进程 需要启动全新程序或注重安全隔离的场景

🧠 工作机制与内存管理

1. fork()

fork() 是类 Unix 系统中创建新进程的传统方式。 * 工作原理fork() 通过复制父进程的地址空间来创建子进程。子进程几乎是父进程的完整副本,包括代码段、数据段、堆栈段和打开的文件描述符等。子进程会获得一个新的进程 ID(PID),并与父进程独立运行。 * 写时复制 (Copy-On-Write, COW):现代操作系统(如 Linux)使用写时复制技术来优化 fork() 的性能和减少内存开销。这意味着: * 在 fork() 之后,父子进程最初共享相同的物理内存页。 * 只有当父进程或子进程试图修改某块内存数据时,操作系统才会为该进程分配新的物理内存页并复制原始数据,然后才执行修改。 * 这种机制避免了不必要的内存复制,大大提高了 fork() 的效率,尤其是在子进程立即调用 exec() 执行新程序的情况下(因为可能很多内存页根本不会被修改)。

2. spawn() (或 posix_spawn())

spawn() 是一组函数(如 posix_spawn()),它提供了另一种启动新进程的方式,通常更加灵活。 * 工作原理spawn() 不复制父进程的地址空间,而是直接加载并执行一个新的程序。新进程从程序的入口点(如 main 函数)开始执行,拥有全新的内存空间。 * 内存管理:由于是全新加载,子进程不继承父进程的数据段、堆或栈。它只有程序本身和任何显式传递的环境变量、参数所定义的内存状态。这自然形成了进程间的隔离。

⚙️ 特点与使用场景

  • fork() 的特点与场景
    • 特点:启动速度快(得益于 COW);子进程继承父进程状态(包括文件描述符,这有时很方便,但也可能导致意外共享资源);在父进程多线程情况下 fork 可能导致某些状态不一致。
    • 典型场景
      • 创建子进程并执行与父进程类似的任务
      • 配合 exec() 系列函数使用:先 fork() 出子进程,然后在子进程中立即调用 exec() 来执行另一个程序。这是一种常见模式,COW 机制在此模式下效率很高。
      • 网络服务中,为每个新连接创建一个新的子进程来处理(例如传统的 CGI 模式)。
  • spawn() 的特点与场景
    • 特点:启动速度相对较慢(需要加载新程序);子进程环境干净,与父进程隔离性好;跨平台支持更好(Windows 也支持类似的机制,虽然 Windows 没有原生的 fork())。
    • 典型场景
      • 需要启动一个完全不同的程序
      • 注重安全性和隔离性,不希望子进程继承父进程的任何状态。
      • 跨平台应用程序中创建新进程。
      • 在涉及 GPU 加速 或操作外部硬件资源 时,使用 spawn 可以避免子进程继承父进程的硬件上下文(如 CUDA 上下文),从而防止潜在的错误和资源冲突。

🧪 代码示例

C 语言中的 fork()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork(); // 创建子进程

if (pid < 0) {
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process: My PID is %d\n", getpid());
// 子进程可以在这里执行任务或调用 exec
} else {
// 父进程
printf("Parent process: My PID is %d, Child's PID is %d\n", getpid(), pid);
wait(NULL); // 等待子进程结束
}
return 0;
}

Python multiprocessing 中设置 spawn

1
2
3
4
5
6
7
8
9
10
11
import multiprocessing as mp

def worker():
print("Worker process executing")

if __name__ == '__main__':
# 设置使用 spawn 方式创建进程
mp.set_start_method('spawn') # 在 Windows 和 macOS 上 spawn 是默认方式
p = mp.Process(target=worker)
p.start()
p.join()

💡 如何选择 fork() 和 spawn()

  1. 优先考虑 spawn:尤其是在编写新代码或需要跨平台时,或者当程序涉及多线程硬件资源(如 GPU) 时,使用 spawn 通常更安全,能避免许多因继承状态而导致的潜在问题。
  2. 谨慎使用 fork():在类 Unix 系统上,如果非常注重创建进程的速度,且能确保 fork 后父进程状态是安全的(例如,没有其他线程在运行,或能妥善处理继承的资源),并且之后很可能调用 exec(),那么 fork() 是一个高效的选择。
  3. 注意平台差异:Windows 系统不支持原生的 fork()。在 Python 等跨语言环境中,Windows 上 multiprocessing 的默认启动方式就是 spawn

💎 总结

fork()spawn() 是两种不同的进程创建哲学: * fork() 类似于“分身”,继承父进程的衣钵,通过写时复制优化内存,在符合其使用场景时非常高效。 * spawn() 类似于“新生”,强调隔离与安全,从头开始加载程序,牺牲一些速度换来更好的稳定性和跨平台支持。

选择哪一种,取决于你的具体需求:是追求极致的创建速度(并在能控制风险的前提下),还是更看重进程的独立性和代码的健壮性。

希望这些信息能帮助你更好地理解 fork() 和 spawn()。如果你还有其他问题,欢迎提出。


协程是什么

协程(Coroutine)是一种比线程更加轻量级的并发编程组件,它允许在单线程内通过协作式调度实现多任务的交替执行。它的核心特点是由程序自身控制任务的挂起与恢复,而非由操作系统内核进行抢占式调度。

为了让你快速把握协程与其他并发概念的区别,我准备了一个对比表格:

特性维度 进程 线程 协程
调度主体 操作系统 操作系统 用户程序(协作式)
资源开销 大(独立内存空间) 中等(共享进程内存) 极小(主要是寄存器上下文和栈)
切换开销 高(需要切换内存空间等) 中(需要内核介入) 极低(无内核切换,用户态完成)
隔离性 (地址空间独立) (共享进程内存) (共享线程内存)
并发性 进程间并发 线程间并发 协程间并发(单线程内)
数据共享 复杂(需要IPC机制) 简单(共享进程内存) 简单(共享线程内存)
抢占机制 抢占式 抢占式 非抢占式(主动让出)
适用场景 计算密集型、需要强隔离 通用型、IO密集型 高并发IO密集型

🧠 协程的核心工作原理

你可以把协程理解为一个可以多次进入、多次返回的函数。它通过 yieldasync/await 这样的关键字,在代码中定义挂起点(Suspension Points)。执行到这些点时,它会主动暂停,保存当前的执行状态(包括局部变量、程序计数器等上下文),并将控制权交还给调度器,而不是被操作系统强制中断。调度器随后可以恢复另一个协程的执行。

这种“主动让出”的协作机制,避免了陷入内核态进行线程上下文切换的巨大开销,使得大量协程在单线程内并发运行成为可能。

🛠️ 实现方式与语法(以Python为例)

不同语言对协程的实现支持不同。在Python中,其演进过程是: 1. 生成器时代:早期通过 yield 关键字模拟协程,需手动调用 .send() 方法传递数据和控制流。 2. 原生协程(Python 3.5+):引入了 asyncawait 关键字,使协程成为语言的一等公民,代码更清晰直观。

一个简单的 Python 协程示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio

async def fetch_data(url): # 使用 async def 定义一个协程函数
print(f"Fetching {url}")
await asyncio.sleep(1) # 使用 await 挂起协程,模拟IO操作
return f"Data from {url}"

async def main():
# 创建多个协程任务
task1 = fetch_data("https://api.example.com/data1")
task2 = fetch_data("https://api.example.com/data2")

# 并发执行这两个任务
results = await asyncio.gather(task1, task2)
print(results)

# 运行主协程
asyncio.run(main())

协程的优缺点

  • 优点
    • 极高的效率:上下文切换在用户态完成,开销极小,可轻松创建数十万协程。
    • 无需复杂的锁机制:由于是单线程内协作,不存在多线程写变量冲突,简化了并发编程。
    • 简化异步代码async/await 语法让异步代码的编写和阅读更接近同步代码,逻辑更清晰。
  • 缺点与注意事项
    • 无法利用多核CPU:协程是单线程的,计算密集型任务仍需依赖多进程或其他方案来并行。
    • 阻塞操作是“杀手”:协程中若存在阻塞式IO长时间计算(未使用 await 或放到其他线程),会阻塞整个事件循环和所有其他协程。
    • 需要异步生态支持:要发挥协程优势,需要配套的异步库(如 aiohttp 替代 requests)。

🚀 协程的适用场景

协程尤其擅长处理IO密集型的高并发任务,例如: - 网络爬虫:同时处理大量网页请求和响应。 - Web服务器(如 FastAPI, Sanic):单线程处理数万并发连接。 - 微服务网关:处理大量上下游API调用和聚合。 - 实时通信:WebSocket服务器,维护大量长连接。

💡 总结一下

协程的核心价值在于,它通过协作式调度用户态切换,在保持代码可读性的同时,极大地提升了IO密集型应用的并发能力和资源利用率。它是现代高并发编程中一项非常重要的技术。

希望这些信息能帮助你理解协程。如果你还有其他问题,欢迎提出。


装饰器怎么写,写一个计算函数运行次数的装饰器

装饰器是 Python 中一个非常实用的特性,它允许你在不修改原函数代码的情况下,为函数添加额外功能。下面我将为你介绍如何编写装饰器,并提供一个用于计算函数运行次数的装饰器示例。

🧩 Python 装饰器:计算函数运行次数

✨ 装饰器简介

装饰器(Decorator)是 Python 中的一种高级语法,它允许您在不修改原函数代码的情况下,动态地扩展函数或类的功能。装饰器本质上是一个可调用对象(通常是函数或类),它接受一个函数作为参数,并返回一个新的函数。

装饰器的核心价值在于遵循开放封闭原则:对扩展开放,对修改封闭。这意味着您可以在不改变原函数代码的情况下,为函数添加新功能。

📊 装饰器类型对比

在深入代码之前,我们先通过一个表格了解几种实现”统计函数执行次数”的方式及其特点:

实现方式 优点 缺点 适用场景
装饰器(函数属性) 优雅简洁,无需修改原函数,多次调用计数持续 理解闭包概念有一定门槛 通用场景,尤其是需要长期统计的情况
装饰器(闭包变量) 同样简洁,使用 nonlocal 关键字 计数无法在函数外部直接持久化 单次运行期间的统计
全局变量 实现简单直观 破坏代码封装性,可能带来全局状态的不一致性 简单脚本或快速原型
类与类属性 面向对象,易于扩展其他功能 稍显繁琐 需要关联更多状态或行为的复杂场景

对于大多数需要统计函数调用次数的场景,使用装饰器是最为优雅和可维护的方式。接下来的示例将采用装饰器(函数属性)的方式。

📝 基本装饰器结构

一个最简单的装饰器通常包含以下结构:

1
2
3
4
5
6
7
8
9
10
11
12
def my_decorator(func):          # 1. 装饰器函数,接受一个函数作为参数
def wrapper(*args, **kwargs): # 2. 内部包装函数,用于增强原函数
# 在调用原函数前可以执行一些代码
result = func(*args, **kwargs) # 调用原函数
# 在调用原函数后可以执行一些代码
return result # 返回原函数的执行结果
return wrapper # 3. 返回包装函数

@my_decorator # 使用装饰器
def my_function():
# 函数体
pass

当使用 @my_decorator 语法糖时,相当于执行了 my_function = my_decorator(my_function)

🔢 计算函数运行次数的装饰器

下面是一个用于统计函数执行次数的装饰器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import functools

def count_calls(func):
"""
统计函数被调用次数的装饰器
"""
@functools.wraps(func) # 使用wraps保留原函数的元信息
def wrapper_count_calls(*args, **kwargs):
wrapper_count_calls.num_calls += 1 # 计数器加1
print(f"调用 {func.__name__!r}{wrapper_count_calls.num_calls} 次")
return func(*args, **kwargs)

wrapper_count_calls.num_calls = 0 # 初始化计数器
return wrapper_count_calls

# 使用装饰器
@count_calls
def say_hello(name):
print(f"Hello, {name}!")
return f"已向 {name} 打招呼"

# 测试装饰器效果
say_hello("Alice")
say_hello("Bob")
say_hello("Charlie")

print(f"say_hello 函数总共被调用了 {say_hello.num_calls} 次")

输出结果:

1
2
3
4
5
6
7
调用 'say_hello'1
Hello, Alice!
调用 'say_hello'2
Hello, Bob!
调用 'say_hello'3
Hello, Charlie!
say_hello 函数总共被调用了 3

🔧 关键技术点说明

  1. 使用函数属性存储计数wrapper_count_calls.num_calls = 0 在包装函数上创建了一个属性来存储调用次数。这是一种在函数调用间保持状态的简便方法。

  2. functools.wraps 的作用@functools.wraps(func) 装饰器用于保留原函数的元数据(如名称、文档字符串等),这是编写装饰器时的最佳实践,有助于调试和保持代码清晰。

  3. 通用参数传递(*args, **kwargs) 确保装饰器可以接受任意数量的位置参数和关键字参数,使其能够装饰任何函数。

🚀 高级用法:带参数的装饰器

如果您需要更灵活地控制装饰器行为,可以创建带参数的装饰器。这需要再多一层嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import functools

def call_counter(print_info=True):
"""
带参数的装饰器工厂函数
:param print_info: 是否打印每次调用信息,默认为True
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.num_calls += 1
if print_info:
print(f"调用 {func.__name__!r}{wrapper.num_calls} 次")
return func(*args, **kwargs)

wrapper.num_calls = 0
return wrapper
return decorator

# 使用带参数的装饰器(打印调用信息)
@call_counter(print_info=True)
def function_one():
print("function_one 执行了")

# 使用带参数的装饰器(不打印调用信息)
@call_counter(print_info=False)
def function_two():
print("function_two 执行了")

# 测试
function_one()
function_two()
function_one()

print(f"function_one 被调用了 {function_one.num_calls} 次")
print(f"function_two 被调用了 {function_two.num_calls} 次")

💡 总结

编写装饰器,尤其是统计函数调用次数的装饰器,主要需注意以下几点: * 核心结构:装饰器是一个返回函数的高阶函数。 * 状态保持:通常使用函数属性(如 func.num_calls)来在多次调用间保持状态。 * 保留元信息:使用 @functools.wraps 保留原函数的名称、文档字符串等元数据。 * 通用性:使用 (*args, **kwargs) 保证装饰器适用于任何函数。

希望以上示例和解释能帮助你理解和编写装饰器。装饰器非常强大,除了计数,还常用于日志记录、性能测试、缓存、权限校验等场景。


多进程多线程的相关知识

多进程和多线程是并发编程中两种核心模型,理解它们的区别和适用场景对编写高效程序很重要。下面我用一个表格汇总它们的核心特性,然后详细解释其原理和适用场景。

特性维度 多进程 (Multiprocessing) 多线程 (Multithreading)
资源分配单位 操作系统资源分配的基本单位 CPU调度和执行的最小单位
数据与内存 内存独立,每个进程有自己独立的地址空间和数据,互不干扰 共享进程资源,所有线程共享同一进程的内存和资源
创建与开销 创建、销毁和切换开销大,占用资源多 创建、销毁和切换开销小,更轻量级
稳定性与安全 更稳定安全,一个进程崩溃不会影响其他进程 稳定性较低,一个线程崩溃可能导致整个进程崩溃
通信与同步 进程间通信(IPC)复杂,需用队列、管道、共享内存等 线程间通信简单,可直接读写共享数据,但需注意同步问题(如锁)
并行性 可实现真正并行,尤其利于利用多核CPU 全局解释器锁(GIL) 等因素限制,常为并发(如CPython)
适用场景 CPU密集型任务(如计算、图像处理)、需稳定隔离的环境 I/O密集型任务(如网络请求、文件读写)、需要快速响应的场景

🧠 核心概念详解

  1. 进程(Process):进程是程序的一次执行过程,是系统进行资源分配和调度的一个独立单位 。每个进程都有独立的地址空间、数据栈以及其他用于跟踪执行的辅助数据。操作系统管理所有进程,并为它们分配资源(如CPU时间、内存)。
  2. 线程(Thread):线程是进程中的一个执行流,是CPU调度和执行的最小单位 。同一进程下的多个线程共享进程的地址空间和资源(如代码段、数据段、打开的文件等),但每个线程拥有自己的栈和局部变量 。

⚖️ 如何选择:多进程 vs. 多线程

选择多进程还是多线程,主要取决于你的任务类型和需求:

  • 选择多进程当
    • 任务是CPU密集型的,需要进行大量计算(如科学计算、图像处理、机器学习模型训练),并且你希望充分利用多核CPU的并行能力 。
    • 需要更高的稳定性和隔离性,避免单个任务失败导致整个应用崩溃 。
  • 选择多线程当
    • 任务是I/O密集型的,涉及大量等待时间(如网络请求、磁盘读写、数据库操作)。线程在等待I/O时可以让出CPU,从而高效处理多个任务。
    • 需要共享大量数据,且线程间的通信和协调较为频繁和直接 。
    • 任务需要快速启动和较低的内存开销

🔧 Python中的实现与注意点(GIL)

在Python中,实现多进程常用 multiprocessing 模块,实现多线程则常用 threading 模块 。

Python的多线程有一个著名的特性——全局解释器锁(GIL)。GIL使得同一时刻只有一个线程可以执行Python字节码 。因此,在CPU密集型任务中,Python的多线程无法利用多核CPU的优势,性能甚至可能不如单线程。但对于I/O密集型任务,由于线程在等待I/O时会释放GIL,GIL的影响较小,多线程依然能有效提升性能 。

若要克服GIL限制,在Python中执行CPU密集型任务,使用多进程是更有效的方式,因为每个进程有自己独立的Python解释器和内存空间,因此也有自己独立的GIL 。

💡 进程/线程间通信与同步

  • 多进程通信(IPC):由于进程间内存独立,需要特殊的进程间通信(IPC)机制,如 multiprocessing.Queue, Pipe, 或由 Manager 管理的共享数据结构 。
  • 多线程同步:因为线程共享内存,所以当多个线程需要修改同一份数据时,必须考虑线程安全问题,避免竞争条件。常用的同步机制包括锁(Lock)信号量(Semaphore)条件变量(Condition) 等 。

🚀 高级模式

  • 进程池(Pool):对于需要创建大量进程的任务,使用 multiprocessing.Pool 可以预先创建并管理一组进程,避免频繁创建和销毁进程的开销,适合处理大量小任务 。
  • 生产者-消费者模型:多进程和多线程都常用此模式,通过队列(Queue)协调生产数据和处理数据的单元 。

希望这些信息能帮助你更好地理解多进程和多线程。如果你有具体的使用场景或编程语言方面的疑问,我很乐意提供更进一步的建议。


Interview questions preparation(1)
http://example.com/2025/09/24/Interview-questions-preparation/
Author
Mingtao Nie
Posted on
September 24, 2025
Licensed under