python
Python
args
import sys
script = sys.argv[0]
if len(sys.argv) > 1 :
target_dir = sys.argv[1]
mode = sys.argv[2] if len(sys.argv) > 2 else "default"
print(target_dir, mode)
File
from pathlib import Path
p = Path(r"D:\data\a.txt")
p.name # 'a.txt'
p.stem # 'a'
p.suffix # '.txt'
p.parent # Path('D:/data')
p.parents[1] # 上两级
p.parts # ('D:\\', 'data', 'a.txt')
p / "sub" / "b.txt" # 拼接,推荐
p = Path(r"C:\Users\32943\Desktop\docs\技术笔记")
# 递归
def loop(path) :
for f in path.iterdir():
if f.is_file():
print(f.name)
elif f.is_dir():
print(f.name)
loop(f.absolute())
loop(p)
# 递归遍历当前目录及子目录下所有的.py文件
for file in Path('.').rglob('*.py'):
print(file)
# 递归 + 过滤
files = [f for f in p.rglob("*") if f.is_file() and f.suffix.lower() in {".md"}]
# 目录
p.mkdir(parents=True, exist_ok=True) # 创建,父目录自动建,已存在不报错
p.rmdir() # 只能删空目录
# 删除
p.unlink(missing_ok=True) # 删文件
shutil.rmtree(p, ignore_errors=False) # 删整棵树,危险
# 复制
shutil.copy2(src, dst) # 复制文件 + 元数据(mtime 等),最常用
shutil.copy(src, dst) # 复制文件,不保留全部元数据
shutil.copytree(src, dst, dirs_exist_ok=True) # 复制整棵树
# 移动/重命名
shutil.move(src, dst) # 跨盘也支持;dst 是目录时移入该目录
p.rename(new_path) # 同盘重命名;跨盘会失败
p.replace(new_path) # 覆盖式重命名(Windows 上原子性有限)
| 需求 | 写法 |
|---|---|
| 拼接路径 | Path(a) / b |
| 绝对路径 | p.resolve() |
| 读文本 | p.read_text(encoding="utf-8") |
| 写文本 | p.write_text(s, encoding="utf-8") |
| 递归找文件 | p.rglob("*.log") |
| 建目录 | p.mkdir(parents=True, exist_ok=True) |
| 删文件 | p.unlink(missing_ok=True) |
| 删目录树 | shutil.rmtree(p) |
| 复制文件 | shutil.copy2(src, dst) |
| 复制目录 | shutil.copytree(src, dst, dirs_exist_ok=True) |
| 移动 | shutil.move(src, dst) |
| 相对路径 | f.relative_to(base) |
| 回收站 | send2trash(str(p)) |
| 大量同步 | subprocess.run(["robocopy", ...]) |
re
rglob
files = [f for f in p.rglob("merge_drve_*.csv") if parse_date(f)]
| 符号 | 名称 | 核心含义 | 示例(rglob 模式) |
匹配示例 |
|---|---|---|---|---|
\* |
星号 | 匹配 0 个或多个 任意字符,但不跨越路径分隔符(/ 或 \)。 |
*.csv |
data.csv, merge_drve_001.csv |
? |
问号 | 精确匹配 任意 1 个 字符。 | data?.csv |
data1.csv, dataA.csv(不匹配 data10.csv) |
[] |
方括号 | 匹配方括号内 列出的任意一个字符。 | file[abc].txt |
filea.txt, fileb.txt, filec.txt |
[-] |
范围 | 在方括号内,用连字符表示一个字符范围。 | report_[0-9].docx |
report_1.docx 到 report_9.docx |
[!] |
否定 | 在方括号内开头使用 !,表示匹配不在列表中的任意一个字符。 |
file[!abc].txt |
filed.txt, file1.txt(不匹配 filea.txt) |
\** |
双星号 | 递归通配符,匹配当前目录及所有子目录。 | **/*.log (在glob()中) |
logs/app.log, logs/2024/01/app.log |
函数:
| 函数 | 作用 | 返回 | 典型用途 |
|---|---|---|---|
re.match(p, s) |
从字符串开头匹配 | Match 或 None | 判断是否以某模式开头 |
re.search(p, s) |
在字符串任意位置找第一个 | Match 或 None | 判断“是否包含” |
re.fullmatch(p, s) |
要求整串完全匹配 | Match 或 None | 校验格式(如日期、编号) |
re.findall(p, s) |
找出所有匹配 | list[str] 或 list[tuple] | 提取所有编号/邮箱 |
re.finditer(p, s) |
找出所有匹配,返回迭代器 | iterator[Match] | 需要位置、分组信息 |
re.sub(p, repl, s) |
替换所有匹配 | str | 清洗文本 |
re.split(p, s) |
按模式切分 | list[str] | 按多种分隔符切分 |
re.compile(p) |
预编译模式 | Pattern | 反复使用时提升效率 |
符号:
| 符号 | 含义 | 例子 | 匹配 |
|---|---|---|---|
. |
任意一个字符(默认不含换行) | a.c |
abc、a1c |
* |
前面元素重复 0 次或多次 | ab*c |
ac、abc、abbbc |
+ |
前面元素重复 1 次或多次 | ab+c |
abc、abbbc |
? |
前面元素 0 或 1 次(可选) | colou?r |
color、colour |
{n} {n,} {n,m} |
精确次数 / 范围 | \d{3} |
123 |
[] |
字符集合 | [abc]、[0-9]、[^0-9] |
一个字符 |
| ` | ` | 或 | `cat |
() |
分组 / 捕获 | (ab)+ |
abab |
^ $ |
行/串首、行/串尾 | ^\d+$ |
整串是数字 |
\d \D |
数字 / 非数字 | \d+ |
2024 |
\w \W |
字母数字下划线 / 非 | \w+ |
merge_drve_01 |
\s \S |
空白 / 非空白 | \s+ |
空格、Tab |
\. |
字面点(转义) | \.csv |
.csv |
*? +? |
非贪婪匹配 | <.*?> |
最短匹配 |
simple :
import re
pattern = re.compile(r"merge_drve_\d+\.csv")
with open("log.txt", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
if pattern.search(line):
print(lineno, line.rstrip())
subprocess
import subprocess
result = subprocess.run(["ls", "-l"])
# 一次性看输出的
import subprocess
def run_capture(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]:
r = subprocess.run(
cmd, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout,
)
return r.returncode, r.stdout, r.stderr
code, out, err = run_capture(["git", "status"])
print(out)
# 实时看输出的
import subprocess, threading
def run_stream(cmd: list[str]) -> int:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1,
)
def pump():
for line in proc.stdout:
print(line, end="", flush=True)
t = threading.Thread(target=pump)
t.start()
proc.wait()
t.join()
return proc.returncode
code = run_stream(["ping", "-c", "5", "127.0.0.1"])
print("返回码:", code)
# 存粹执行命令
import subprocess
# 检查错误
subprocess.run(["your_cmd", "arg1", "arg2"], check=True)
# 不检查错误
try:
subprocess.run(["make", "build"], check=True)
print("成功")
except subprocess.CalledProcessError as e:
print(f"失败,返回码 {e.returncode}")
except FileNotFoundError:
print("命令不存在")
| 参数 | 作用 | 说明 |
|---|---|---|
args |
命令 | 列表(推荐)或字符串(配 shell=True) |
capture_output=True |
捕获 stdout+stderr | 等价 stdout=PIPE, stderr=PIPE(3.7+) |
stdout=PIPE / stderr=PIPE |
单独捕获 | |
text=True |
输出解码为 str | 等价 universal_newlines=True |
encoding="utf-8" |
指定编码 | 比 text=True 更明确 |
check=True |
返回码非 0 抛异常 | 否则你得自己判 returncode |
timeout=30 |
超时秒数 | 超时抛 TimeoutExpired |
cwd="/path" |
工作目录 | |
env={...} |
环境变量 | 会替换而非合并,通常配 os.environ.copy() |
input="..." |
传给 stdin 的内容 | 需配 text=True |
shell=True |
走系统 shell | ⚠️ 有注入风险,见下文 |
stdin=subprocess.DEVNULL |
不读 stdin | 防止卡住 |
errors="ignore" |
解码错误处理 | 处理乱码输出 |
configparser
必须有【sector】 【key】 【value】
假设 config.ini:
[default]
keep_id = 2
name = demo
debug = true
timeout = 30
[server]
host = 127.0.0.1
port = 8080
使用:
import configparser
# read file
config = configparser.ConfigParser()
config.read("config.ini", encoding="utf-8")
# get
config["server"]["host"] # 推荐写法,SectionProxy
config.get("server", "host") # 等价
config.get("server", "missing", fallback="默认值") # 带默认值
config.getint("server", "port") # 8080
config.getfloat("default", "ratio") # 1.5
config.getboolean("default", "debug") # True
# update
config["default"]["keep_id"] = "0"
config.set("default", "keep_id", "0")
# add
config.add_section("new_section")
config["new_section"]["key"] = "value"
# delete
config.remove_option("server", "port")
config.remove_section("client")
config.clear() # 清空全部
# write file
with open("config.ini", "w", encoding="utf-8") as f:
config.write(f)
关键点:configparser 读出来的值永远是字符串,要数字得自己转。
解析csv
data.csv:
time(s),event(w/r/e),IOPS
0.0, w, 3680
0.2, r, 5112
0.4, e, 7362
0.6, r, 8963
0.8, r, 5921
import csv
with open("data.csv", "r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
# for row in reader:
# print(row)
data = [
[
float(r['time(s)']),
r['event(w/r/e)'].strip(),
int(r['IOPS'].strip()),
]
for r in reader
]
pass
基础
Pandas
读取文件
| 读取函数 | 写入函数 | 支持格式 | 典型场景 |
|---|---|---|---|
pd.read_csv() |
to_csv() |
CSV、TSV | 日志文件、表格数据 |
pd.read_excel() |
to_excel() |
Excel(.xlsx, .xls) | 业务报表、财务数据 |
pd.read_sql() |
to_sql() |
SQL 数据库 | 企业数据库交互 |
pd.read_html() |
- | HTML 表格 | 网页数据抓取 |
pd.read_parquet() |
to_parquet() |
Apache Parquet | 大数据分析、存储 |
pd.read_feather() |
to_feather() |
Feather 格式 | 快速读写、内存数据 |
pd.read_json() |
to_json() |
JSON | API 数据、Web 服务 |
pd.read_pickle() |
to_pickle() |
Python pickle | 序列化 Python 对象 |
example :
# 读取
df = pd.read_csv("data.csv")
print(df)
# 写入 CSV(默认有索引) 不写入索引
df = pd.DataFrame({
"姓名": ["张三", "李四", "王五"],
"年龄": [25, 30, 28],
"城市": ["北京", "上海", "广州"]
})
df.to_csv("output.csv", index=False)
数据选取
loc / iloc / at / iat
| 特性 | loc | iloc |
|---|---|---|
| 索引方式 | 标签索引(label-based) | 位置索引(integer-based) |
| 切片 | 包含结束位置 | 不包含结束位置 |
| 单个值 | 返回标量 | 返回标量 |
| 推荐场景 | 有明确索引标签时 | 按位置选择时 |
import pandas as pd
df = pd.DataFrame({
"姓名": ["张三", "李四", "王五", "赵六"],
"年龄": [25, 30, 28, 35],
"城市": ["北京", "上海", "广州", "深圳"]
}, index=["a", "b", "c", "d"])
# 选择特定行列
print("选择单个值:")s
print(df.loc["a", "姓名"]) # 返回 "张三"
print(type(df.loc["a", "姓名"])) # 类型是 str
print()
# 选择多行多列
print("选择子集:")
print(df.loc[["a", "c"], ["姓名", "城市"]])
print()
# 条件选择
print("年龄大于28的行:")
print(df.loc[df["年龄"] > 28])
at 和 iat 是专门用于获取/设置单个值的访问器,比 loc 和 iloc>更快。
import pandas as pd
import time
import numpy as np
# 创建大 DataFrame 用于性能测试
df = pd.DataFrame(np.random.randn(1000, 10), columns=[f"col_{i}" for i in range(10)])
# at 获取单个值(标签索引)
print(f"at: {df.at[0, 'col_0']}")
# iat 获取单个值(位置索引)
print(f"iat: {df.iat[0, 0]}")
# 性能对比
n = 10000
start = time.time()
for _ in range(n):
_ = df.iloc[0, 0]
print(f"iloc 耗时: {time.time() - start:.4f}s")
start = time.time()
for _ in range(n):
_ = df.iat[0, 0]
print(f"iat 耗时: {time.time() - start:.4f}s")
NumPy
NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。
安装
pip3 install numpy
数组计算
Matplotlib
Matplotlib 是一个强大的 Python 绘图库,用于创建各种类型的静态、动态和交互式图表。
安装
python3 -m pip install -U matplotlib
图形
# simple
import matplotlib.pyplot as plt
import numpy as np
xpoints = np.array([0, 6])
ypoints = np.array([0, 100])
plt.plot(xpoints, ypoints)
plt.show()
以下是一些常用的 pyplot 函数:
plt.plot(x, y):用于绘制线图和散点图plt.scatter(x, y):用于绘制散点图plt.bar(x,y):用于绘制垂直条形图和水平条形图plt.hist(data, bins=30, color='skyblue', alpha=0.8):用于绘制直方图plt.pie(y):用于绘制饼图subplots():用于创建子图
plot:
以上实例中我们使用了 Pyplot 的 plot() 函数, plot() 函数是绘制二维图形的最基本函数。
plot() 用于画图它可以绘制点和线,语法格式如下:
# 画单条线
plot([x], y, [fmt], *, data=None, **kwargs)
# 画多条线
plot([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs)
参数说明:
- x, y:点或线的节点,x 为 x 轴数据,y 为 y 轴数据,数据可以列表或数组。
- fmt:可选,定义基本格式(如颜色、标记和线条样式)。
- **kwargs:可选,用在二维平面图上,设置指定属性,如标签,线的宽度等。
>>> plot(x, y) # 创建 y 中数据与 x 中对应值的二维线图,使用默认样式
>>> plot(x, y, 'bo') # 创建 y 中数据与 x 中对应值的二维线图,使用蓝色实心圈绘制
>>> plot(y) # x 的值为 0..N-1
>>> plot(y, 'r+') # 使用红色 + 号
颜色字符:'b' 蓝色,'m' 洋红色,'g' 绿色,'y' 黄色,'r' 红色,'k' 黑色,'w' 白色,'c' 青绿色,'#008000' RGB 颜色符串。多条曲线不指定颜色时,会自动选择不同颜色。
线型参数:'‐' 实线,'‐‐' 破折线,'‐.' 点划线,':' 虚线。
标记字符:'.' 点标记,',' 像素标记(极小点),'o' 实心圈标记,'v' 倒三角标记,'^' 上三角标记,'>' 右三角标记,'<' 左三角标记...等等。
如果我们要绘制坐标 (1, 3) 到 (8, 10) 的线,我们就需要传递两个数组 [1, 8] 和 [3, 10] 给 plot 函数:
绘图过程如果我们想要给坐标自定义一些不一样的标记,就可以使用 plot() 方法的 marker 参数来定义。
: plt.plot(ypoints, marker = 'o') 实心圆
绘图过程如果我们自定义线的样式,包括线的类型、颜色和大小等。
:plt.plot(ypoints, linestyle = 'dotted')点虚线
- 线的颜色可以使用 color 参数来定义,简写为 c。
plt.plot(ypoints, color = 'r')红色
global:
- 可以使用 xlabel() 和 ylabel() 方法来设置 x 轴和 y 轴的标签。
plt.xlabel("x - label")、plt.ylabel("y - label")x 轴和 y 轴的标签
可以使用 title() 方法来设置标题。
:plt.title("TEST TITLE") 标题
可以使用 pyplot 中的 grid() 方法来设置图表中的网格线
:plt.grid() 网格线
- 可以使用 pyplot 中的 subplot() 和 subplots() 方法来绘制多个子图
subplot(nrows, ncols, index, **kwargs)整个绘图区域分成 nrows 行和 ncols 列,然后从左到右,从上到下的顺序对每个子区域进行编号 1...N
img :
- 可以使用pyplot中的imshow用于显示图像。
plt.imshow(img)显示图片- 可以使用pyplot中的imsave用于保存图像。
plt.imsave('test.png', img_data)保存图片- 可以使用pyplot中的imread用于读取图像。
img = plt.imread('map.jpeg')读取图片
Plotly
交互图
安装
pip install plotly
ndarray 对象属性
| 属性 | 说明 | 例子 |
|---|---|---|
ndarray.ndim |
数组的维度数量,也称为秩或轴数。 | a = np.arange(24)a.ndim # 1b = a.reshape(2, 4, 3)b.ndim # 3 |
ndarray.shape |
数组在每个轴上的大小,返回元组。二维数组通常表示为行数和列数。 | a = np.array([[1, 2, 3], [4, 5, 6]])a.shape # (2, 3)a.reshape(3, 2) |
ndarray.size |
数组中元素的总个数,等于各轴大小的乘积。 | a.shape # (2, 3)a.size # 6 |
ndarray.dtype |
数组元素的数据类型。 | a = np.array([1, 2, 3])a.dtype # int64b = np.array([1, 2, 3], dtype=np.float32)b.dtype # float32 |
ndarray.itemsize |
每个数组元素占用的字节数。 | x = np.array([1, 2, 3], dtype=np.int8)x.itemsize # 1y = np.array([1, 2, 3], dtype=np.float64)y.itemsize # 8 |
ndarray.nbytes |
数组数据占用的总字节数,等于 size × itemsize。 |
x = np.array([1, 2, 3], dtype=np.int32)x.nbytes # 12 |
ndarray.real |
返回数组元素的实部,主要用于复数数组。 | a = np.array([1+2j, 3+4j])a.real # [1. 3.] |
ndarray.imag |
返回数组元素的虚部,主要用于复数数组。 | a = np.array([1+2j, 3+4j])a.imag # [2. 4.] |
ndarray.data |
数组实际存储元素的缓冲区,一般不直接使用。 | a.data |
ndarray.flags |
返回数组的内存布局和读写状态等信息。 | x.flags可查看 C_CONTIGUOUS、F_CONTIGUOUS、OWNDATA、WRITEABLE 等状态。 |
C_CONTIGUOUS |
数据按 C 语言风格连续存储,即按行优先存储。 | x.flags.c_contiguous # True |
F_CONTIGUOUS |
数据按 Fortran 语言风格连续存储,即按列优先存储。 | x.flags.f_contiguous # True |
OWNDATA |
表示数组是否拥有自己的数据内存。 | x.flags.owndata # True |
WRITEABLE |
表示数组数据是否可以修改。 | x.flags.writeable # True |
ALIGNED |
表示数据和元素是否按照硬件要求正确对齐。 | x.flags.aligned # True |
WRITEBACKIFCOPY |
表示该数组是否是另一个数组的副本,释放时可能将数据写回原数组。 | x.flags.writebackifcopy # False |
PyNVMe
概念
pynvme 是一个面向 SSD 开发和测试的 NVMe 用户态测试框架。它让你用 Python 和 pytest 编写测试,同时直接向 NVMe 控制器发送命令。除了读写数据,也可以查询 Identify 数据、读取日志页、控制 I/O 队列、运行持续负载,或测试控制器行为。
它不只是“用 Python 包装一下磁盘读写”。它的目标是让测试代码能更直接地控制 NVMe 命令和队列,并能承受较高的 I/O 压力。官方项目把它描述为带 Python API 的用户态 NVMe 测试驱动。
适合用它做的事情包括:
- 检查控制器和命名空间是否正确响应命令。
- 验证读写结果、数据模式和数据完整性。
- 观察不同 I/O 大小、队列深度和读写比例下的表现。
- 读取 SMART、错误日志等信息。
- 用 pytest 组织测试、参数化测试和自动化测试流程。
它不是最简单的通用磁盘测速工具。如果只是想测一下日常磁盘速度,普通系统工具可能更合适;如果需要直接控制 NVMe 行为、构造测试或做设备验证,PyNVMe 才更有用。
NVMe 设备层级大致可以这样理解:
PCIe 设备
└── NVMe Controller(控制器)
└── Namespace(命名空间)
└── LBA(逻辑块地址)
CI 地址 / BDF : 操作系统通过 PCIe 枚举设备。PyNVMe 通常需要你指定测试 SSD 的 PCI 地址,例如 01:00.0。它必须对应你打算测试的那块设备。
Controller : NVMe 控制器负责处理管理命令和创建 I/O 队列。代码中常见名字是 nvme0。
Namespace : 命名空间是控制器提供的逻辑存储空间。读写命令通常发给 namespace,常见名字是 nvme0n1。
LBA : 读写地址以逻辑块为单位。LBA 0 表示第一个逻辑块,不是 Python 列表下标。每个 LBA 有多少字节要看设备的逻辑块大小;常见例子会使用 512 字节或 4096 字节,但不要未经检查就假设所有设备一致。
Qpair : I/O Queue Pair 包含提交命令的 Submission Queue 和接收完成结果的 Completion Queue。队列深度 qdepth 决定队列里最多能同时挂起多少命令。通常队列深度越高,设备可以承受的并发越大,但测试结果和设备负载也会改变。
Buffer : 通过 DMA 和设备交换数据的缓冲区。PyNVMe 的 Buffer 会处理普通 Python 对象不适合直接用于设备 DMA 的问题。
pytest fixture : PyNVMe 利用 pytest fixture 帮你准备测试所需的对象。测试函数参数里的 nvme0、nvme0n1、qpair 等名字不是随便起的,它们通常由 pytest 插件自动创建和管理。
层次
设备访问层:PCIe、Controller、Namespace、Buffer、Qpair。
命令测试层:NVMe 管理命令、I/O 命令、通用和厂商命令。
负载测试层:IOWorker、并发队列、IOPS、延迟、数据校验。
系统与协议测试层:复位、电源循环、错误场景、PSD 低层队列测试
安装
git clone https://github.com/pynvme/pynvme
cd pynvme
./install.sh
make setup
make test TESTS="driver_test.py::test_ioworker_iops_multiple_queue[1]"
常见命令
- 直接发送命令
直接使用 read()、write()、getlogpage() 等接口,适合学习命令流程,或测试某一条命令的特定行为。
概念流程一般是:
准备数据缓冲区
→ 提交 NVMe 命令
→ 等待命令完成
→ 检查状态和结果
- 使用 IOWorker 产生负载
ioworker 是高层级 I/O 工作负载接口。你设置工作负载参数,它在后台持续发命令,结束后返回统计结果。它适合测 IOPS、延迟分布和负载变化;一个 worker 会使用自己的 I/O queue,因此 worker 数量也受到控制器可用队列数量的限制。【(github.com)】
例如,下面展示的是纯读取负载:
def test_read_load(nvme0n1):
result = (
nvme0n1.ioworker(
io_size=8,
lba_align=8,
lba_random=True,
read_percentage=100,
qdepth=16,
region_start=TEST_START_LBA,
region_end=TEST_END_LBA,
time=10,
)
.start()
.close()
)
assert result.error == 0
这段代码用占位符表示测试区域的起止 LBA,你必须先计算并确认范围,不能把占位符直接运行。
参数含义:
io_size=8:每个请求大小为 8 个 LBA。若设备的 LBA 大小是 512 字节,则请求是 4096 字节;其他 LBA 大小下结果不同。lba_align=8:请求地址按 8 个 LBA 对齐。lba_random=True:在指定范围里随机选择起始 LBA。read_percentage=100:全读,不发送写请求。qdepth=16:每个 worker 的队列深度为 16。region_start、region_end:工作负载的 LBA 范围;region_end是不包含的边界。time=10:运行约 10 秒。.start():启动 worker。.close():等待 worker 结束并取回结果。result.error:工作负载返回的错误码;还应检查实际 I/O 数量和测试日志。
结果分析:
IOWorker 结果里常见的信息包括:
io_count_read:读取命令数量。io_count_write:写入命令数量。io_count_nonread:非读取命令数量,具体包含哪些操作要结合工作负载。mseconds:实际运行时间,毫秒。latency_average_us:平均延迟,微秒。latency_max_us:最大延迟,微秒。error:执行状态或错误码。
IOPS 的粗略计算方式是:
IOPS ≈ 完成的 I/O 数量 ÷ 实际持续时间(秒)
例如,若 10 秒完成 100 万个请求,粗略结果是 100,000 IOPS。但分析性能不能只看一个总数,还要记录请求大小、读写比例、随机或顺序访问、队列深度、worker 数量、LBA 范围、测试时长和设备温度。否则两次结果可能根本不是同一类负载。
要看延迟分布,可以在 API 支持时传入 output_percentile_latency,例如收集 50、90、99、99.9 百分位。百分位延迟通常比平均值更能反映长尾响应时间;但高百分位统计也要求足够多的样本。IOWorker 文档说明它能返回运行统计,并支持额外的每秒 I/O 和延迟百分位输出参数。
Simple
import nvme as d
def test_read_write(nvme0, nvme0n1, qpair):
write_buf = d.Buffer(512)
read_buf = d.Buffer(512)
write_buf[10:21] = b"hello world"
nvme0n1.write(qpair, write_buf, 0, 1).waitdone()
nvme0n1.read(qpair, read_buf, 0, 1).waitdone()
assert read_buf[10:21] == b"hello world"
PyTest
基础
python的命名规则
1)py文件全部小写,多个英文用_隔开
2)class名首字母大写,驼峰
3)函数和方法名小写,多个英文用_隔开
4)全局变量,前面要加global
5)常量字母必须全大写,如:AGE_OF_NICK
- 文件/函数/类命名强制规范:
- 文件名:
test_*.py或*_test.py - 函数名:
test_* - 类名:
Test*(且不能有__init__构造函数)
- 文件名:
- 收集机制:运行
pytest --collect-only可以查看 Pytest 到底找到了哪些用例(用来排查“collected 0 items”的神器)。 - 命令行选跑:
pytest test_mod.py::test_func可以精准执行某个具体用例,不用全跑。
安装
python3 -m venv .venv
source .venv/bin/activate
python -m pip install pytest
主函数运行
main中可使用的参数有:
| 参数 | 描述 | 案例 |
|---|---|---|
-v |
输出调试信息。如:打印信息 | pytest.main(['-v','testcase/test_one.py','testcase/test_two.py']) |
-s |
输出更详细的信息,如:文件名、用例名 | pytest.main(['-vs','testcase/test_one.py','testcase/test_two.py']) |
-n |
多线程或分布式运行测试用例 | (未提供) |
-x |
只要有一个用例执行失败,就停止执行测试 | pytest.main(['-vsx','testcase/test_one.py']) |
--maxfail |
出现N个测试用例失败,就停止测试 | pytest.main(['-vs','-x=2','testcase/test_one.py']) |
--html=report.html |
生成测试报告 | pytest.main(['-vs','--html=./report.html','testcase/test_one.py']) |
-m |
通过标记表达式执行 | |
-k |
根据测试用例的部分字符串指定测试用例,可以使用and,or | pytest -k “MyClass and not method”,这条命令会匹配文件名、类名、方法名匹配表达式的用例,这里这条命令会运行 TestMyClass.test_something, 不会执行 TestMyClass.test_method_simple |
标记系统(Marks)
你之前接触的 skip、skipif、parametrize 都属于这个模块,但远不止这些:
| 标记 | 作用 | 关键时刻 |
|---|---|---|
@pytest.mark.parametrize |
数据驱动,一组数据生成一条用例(你之前问过的) | 测试登录不同账号、不同边界值 |
@pytest.mark.skip |
无条件跳过 | 功能未完成或环境缺失 |
@pytest.mark.skipif |
满足条件时跳过(如 Python 版本低) | 兼容性测试 |
@pytest.mark.xfail |
预期失败(执行但不报错,显示 XFAIL) | 已知 Bug 尚未修复,不阻塞 CI |
@pytest.mark.usefixtures |
自动加载 Fixture(不需要返回值) | 只为了执行前置/后置,不依赖返回值时 |
自定义标记(如 @pytest.mark.smoke) |
打标签,按组筛选运行 | pytest -m "smoke" 只跑冒烟测试 |
重要配置:自定义标记需要在 pytest.ini 中注册,否则会有警告。
[pytest]
markers =
smoke: 冒烟测试,验证核心功能
regression: 回归测试,验证已有功能未退化
conftest.py —— 提供一个 Fixture(供 usefixtures 使用)
import pytest
@pytest.fixture(scope="function")
def setup_and_teardown():
"""一个只执行前后置动作、不返回任何值的 Fixture"""
print("\n🟢 准备测试环境...")
yield
print("\n🔴 清理测试环境...")
test_marks.py
import pytest
import sys
# --------------------------------------------------------------------
# 1. @pytest.mark.parametrize —— 数据驱动测试
# --------------------------------------------------------------------
@pytest.mark.parametrize("account, passwd, expected", [
("admin", "123456", True),
("guest", "guest", True),
("invalid", "wrong", False),
])
def test_login(account, passwd, expected):
# 模拟登录验证
def fake_login(a, p):
return (a == "admin" and p == "123456") or (a == "guest" and p == "guest")
assert fake_login(account, passwd) is expected
# --------------------------------------------------------------------
# 2. @pytest.mark.skip —— 无条件跳过
# --------------------------------------------------------------------
@pytest.mark.skip(reason="该功能尚未实现")
def test_new_feature():
assert False # 不会执行
# --------------------------------------------------------------------
# 3. @pytest.mark.skipif —— 条件跳过
# --------------------------------------------------------------------
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8 或更高版本")
def test_python38_only():
assert sys.version_info >= (3, 8)
# --------------------------------------------------------------------
# 4. @pytest.mark.xfail —— 预期失败(不阻塞 CI)
# --------------------------------------------------------------------
@pytest.mark.xfail(reason="已知 Bug #123,待修复")
def test_known_bug():
assert 1 + 1 == 3 # 注定失败,但标记为 XFAIL,不会让整体报告为失败
# --------------------------------------------------------------------
# 5. @pytest.mark.usefixtures —— 只执行前后置,不要返回值
# --------------------------------------------------------------------
@pytest.mark.usefixtures("setup_and_teardown")
def test_with_cleanup():
print(" 📝 正在执行测试主体...")
assert True
# --------------------------------------------------------------------
# 6. 自定义标记 —— @pytest.mark.smoke / regression
# --------------------------------------------------------------------
@pytest.mark.smoke
def test_core_api():
assert 2 * 2 == 4
@pytest.mark.regression
def test_old_feature():
assert "hello".upper() == "HELLO"
# 也可以组合使用多个标记
@pytest.mark.smoke
@pytest.mark.regression
def test_combined():
assert len([1, 2, 3]) == 3
pytest.ini配置文件
| 参数 | 作用 |
|---|---|
[pytest] |
用于标志这个文件是pytest的配置文件 |
addopts |
命令行参数,多个参数之间用空格分隔 |
testpaths |
配置搜索参数用例的范围 |
python_files |
改变默认的文件搜索规则 |
python_classes |
改变默认的类搜索规则 |
python_functions |
改变默认的测试用例的搜索规则 |
markers |
用例标记,自定义mark,需要先注册标记,运行时才不会出现warnings |
在项目的根目录下创建pytest.ini文件
[pytest]
addopts=-vs -m slow --html=./report/report.html
testpaths=testcase
test_files=test_*.py
test_classes=Test*
test_functions=test_*
makerers=smock:冒烟测试用例
fixture
- 五大作用域:
function(默认)、class、module、package、session。 - 生命周期清理:
yield分隔 setup 和 teardown(安全可靠)。 - 依赖注入链:一个 Fixture 可以依赖另一个 Fixture(如
login_user依赖browser)。 - 自动执行:
autouse=True让 Fixture 不用声明就自动跑(常用于监控、计时)。 - 共享仓库:
conftest.py是 Fixture 的“集散地”,同目录及其子目录自动可见。 - 内置实用 Fixture:
tmp_path:临时文件/目录monkeypatch:运行时动态修改环境变量/属性(打 Mock 补丁)capsys:捕获标准输出(测试打印语句)request:获取测试上下文(如参数化的request.param)
生命周期与作用域(Scope)
这是资源配置效率的关键,通过 scope 参数控制:
| 作用域 | 触发时机 | 适用场景 |
|---|---|---|
function(默认) |
每个测试函数执行前后 | 轻量级资源,如临时文件、Mock 对象。 |
class |
每个测试类执行前后 | 类级别的客户端实例(如 Django Client)。 |
module |
每个 .py 文件执行前后 |
模块级别的数据库连接。 |
package |
每个包执行前后 | 较少用,用于包级共享。 |
session |
整个测试会话执行前后 | 重量级资源,如启动一次浏览器驱动或 Docker 容器。 |
@pytest.fixture(scope="module")
@pytest.fixture(autouse=True) # autouse=True`让 Fixture 不用声明就自动跑(常用于监控、计时)
fixture 中最优雅的清理方式有两种:
-
yield 语法(推荐):
yield之前的代码相当于setup,yield之后的代码相当于teardown。必须确保yield交出资源。python
@pytest.fixture def db_conn(): conn = create_conn() yield conn # 测试在此处执行 conn.close() # 测试结束后自动清理 -
addfinalizer 机制:适合需要注册多个清理函数的场景,即使 setup 失败也会尝试执行。
Fixture 的参数化:这是常被忽略的强大功能:让同一个 fixture 产生多组不同数据,测试会自动跑多轮。
@pytest.fixture(params=["chrome", "firefox", "edge"])
def browser(request):
# request.param 接收参数值
driver = start_driver(request.param)
yield driver
driver.quit()
# 测试函数会执行 3 次,分别传入不同浏览器
def test_login(browser):
...
共享与覆盖(conftest.py 的妙用)
- 共享:将多个 fixture 放在项目根目录或测试子目录的
conftest.py中,同目录及子目录下的所有测试都可自动调用(无需 import)。 - 覆盖(优先级):如果在测试文件的内部定义了同名 fixture,会覆盖
conftest.py中的定义。利用这点可以在不同环境下(如本地 vs CI)使用不同的配置。

浙公网安备 33010602011771号