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 # 1
b = 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 # int64
b = np.array([1, 2, 3], dtype=np.float32)
b.dtype # float32
ndarray.itemsize 每个数组元素占用的字节数。 x = np.array([1, 2, 3], dtype=np.int8)
x.itemsize # 1
y = 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]"

常见命令

  1. 直接发送命令

直接使用 read()、write()、getlogpage() 等接口,适合学习命令流程,或测试某一条命令的特定行为。

概念流程一般是:

准备数据缓冲区
→ 提交 NVMe 命令
→ 等待命令完成
→ 检查状态和结果
  1. 使用 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

  1. 五大作用域:function(默认)、class、module、package、session。
  2. 生命周期清理:yield 分隔 setup 和 teardown(安全可靠)。
  3. 依赖注入链:一个 Fixture 可以依赖另一个 Fixture(如 login_user 依赖 browser)。
  4. 自动执行:autouse=True 让 Fixture 不用声明就自动跑(常用于监控、计时)。
  5. 共享仓库:conftest.py 是 Fixture 的“集散地”,同目录及其子目录自动可见。
  6. 内置实用 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)使用不同的配置。
posted @ 2026-11-08 21:51  aswd  阅读(3)  评论(0)    收藏  举报