2、Python基础回顾:数据类型、控制流、函数、模块与包管理
自动化测试与仪器控制的本质,是使用程序精确地描述“读取数据—做出判断—执行动作—记录结果”这一闭环。Python之所以成为该领域的首选语言,并非因为其语法最简单,而是因为其数据类型能够自然映射仪器返回的数值与字符串、控制流能够模拟复杂的时序逻辑、函数与模块机制则让测试代码具备可维护性和复用性。本章将系统梳理这五个核心主题,并始终围绕“为后续编写自动化脚本打基础”这一目标展开。我们不仅会回顾语法,更会从工程视角审视:如何选择合适的数据结构、如何设计控制逻辑、如何组织代码文件,从而避免在后续章节中因基础不牢而陷入调试泥潭。
上图展示了本章的知识框架。数据类型是描述仪器数据的“语言”,控制流是脚本的“决策中枢”,函数是“逻辑封装单元”,模块与包管理则是“工程组织方式”。四个部分环环相扣,最终统一服务于自动化测试脚本的健壮性、可读性与可维护性。下面我们逐一深入。
2.1 数据类型:精准描述仪器世界的基石
核心原理:数据类型的本质是内存布局与操作语义
Python中的每个变量都是对象的引用,而对象由其类型决定内存占用和可用操作。在仪器控制场景中,我们最常打交道的类型包括:int(整数,如计数器读数)、float(浮点数,如电压值)、str(字符串,如仪器返回的SCPI命令响应)、list(列表,如扫描得到的波形数据点)、tuple(元组,如固定通道配置)、dict(字典,如通道号到物理量的映射)、set(集合,用于去重或成员判断)。理解可变与不可变对象至关重要:list、dict、set可变,而int、float、str、tuple不可变。这直接影响函数传参时的行为——可变对象在函数内修改会影响到外部变量。
判断方法:根据数据特征选择合适容器
面对一组仪器数据,选择哪种容器?可遵循以下判断路径:
- 是否需要保持顺序? 是→用
list或tuple;否→考虑set或dict。 - 是否需要按键访问? 是→用
dict,键可以是通道名或参数名。 - 数据是否允许重复? 不允许→用
set;允许→用list。 - 数据是否会被修改? 不会→用
tuple,既安全又节省内存。
list存储原始数据,再通过列表推导式转换为float或int。不要试图用字符串拼接处理数值,容易引入精度问题。
案例拆解:解析万用表返回的字符串
假设一台数字万用表通过串口返回如下字符串:"CH1:1.2345V,CH2:0.9876V,CH3:2.3456V"。我们需要提取每个通道的电压值并计算平均值。初始数据是字符串,目标数据结构是字典和列表。
raw_data = "CH1:1.2345V,CH2:0.9876V,CH3:2.3456V"
# 第一步:按逗号分割
segments = raw_data.split(",")
# 第二步:解析每个通道
voltages = {}
for seg in segments:
ch, val = seg.split(":")
voltages[ch] = float(val.rstrip("V"))
# 第三步:计算平均值
avg = sum(voltages.values()) / len(voltages)
print(f"平均电压: {avg:.4f}V")
这个案例展示了字符串分割、字典存储、类型转换的完整链路。如果数据量更大,还可以用列表推导式简化:vals = [float(s.split(":")[1].rstrip("V")) for s in raw_data.split(",")]。
常见误区与复盘
- 误区1:混淆
==与is。对于小整数和短字符串,Python会缓存对象,但长字符串或大整数不保证。判断内容用==,判断身份用is。 - 误区2:在循环中修改列表长度。例如
for item in my_list: my_list.remove(item)会导致跳过元素。应使用列表推导式创建新列表。 - 误区3:将浮点数直接用于相等比较。仪器读数应使用
abs(a-b) < 1e-6而非a == b。
2.2 控制流:构建测试逻辑的决策树与循环引擎
核心原理:控制流决定脚本的执行路径
自动化测试脚本本质上是“状态机”——根据当前输入决定下一步动作。if/elif/else用于条件分支,for用于可迭代对象遍历,while用于条件循环。在仪器控制中,for循环常用于扫描多个通道或重复测量,while循环常用于等待仪器就绪或数据稳定。需要特别注意的是break(跳出整个循环)与continue(跳过本次迭代)的区别,以及else子句在循环中的特殊语义——循环正常结束(未被break中断)时执行。
操作流程:设计健壮的循环结构
- 明确循环终止条件:对于
while循环,务必设置最大迭代次数或超时时间,防止死循环。 - 优先使用
for而非while:当迭代次数已知时,for更简洁且不易出错。 - 利用
enumerate获取索引:需要同时获取元素和索引时,使用for i, val in enumerate(data)。 - 使用
zip并行遍历:同时遍历多个列表时,for a, b in zip(list1, list2)。
案例拆解:带超时的仪器状态等待
在自动化测试中,经常需要等待仪器完成自检或进入远程模式。以下代码展示了while循环与超时控制的结合:
import time
def wait_for_instrument_ready(timeout=10, poll_interval=0.2):
start = time.time()
while time.time() - start < timeout:
status = query_instrument_status() # 假设返回"READY"或"BUSY"
if status == "READY":
print(f"仪器就绪,耗时{time.time()-start:.2f}秒")
return True
time.sleep(poll_interval)
print("错误:等待超时")
return False
这个案例体现了while循环、time.time()超时判断、time.sleep()轮询间隔三个关键点。如果没有超时机制,仪器故障时脚本将永远挂起。
常见误区与复盘
- 误区1:在
for循环内修改正在遍历的列表。应遍历副本for item in my_list[:]。 - 误区2:忘记
break导致死循环。在while True中务必有退出条件。 - 误区3:过度使用嵌套循环。三层以上嵌套应重构为函数调用或使用
itertools.product。
2.3 函数:封装可复用的测试动作
核心原理:函数是逻辑复用的最小单元
函数将一段完成特定功能的代码封装起来,通过参数传递输入、通过返回值传递输出。在自动化测试中,函数用于封装“连接仪器”“读取数据”“设置参数”“断开连接”等原子操作。Python函数支持位置参数、关键字参数、默认参数、*args和**kwargs。理解作用域规则(LEGB:Local→Enclosing→Global→Built-in)是避免变量污染的关键。
设计方法:编写高质量测试函数
- 单一职责原则:一个函数只做一件事。例如
read_voltage(channel)不应同时负责切换通道和记录日志。 - 参数显式化:避免使用全局变量传递数据,通过参数传入。
- 返回值明确:成功返回数据,失败返回
None或抛出异常,不要混合返回类型。 - 使用类型注解:
def read_voltage(channel: int) -> float:提高可读性。
案例拆解:封装SCPI命令发送函数
SCPI(可编程仪器标准命令)是仪器控制的核心协议。以下函数封装了发送命令并读取响应的通用操作:
import pyvisa
def scpi_query(inst, command: str, timeout: float = 5.0) -> str:
"""
发送SCPI命令并读取响应
:param inst: pyvisa仪器对象
:param command: SCPI命令字符串
:param timeout: 超时秒数
:return: 仪器返回的字符串
"""
try:
inst.timeout = timeout * 1000 # pyvisa使用毫秒
response = inst.query(command)
return response.strip()
except pyvisa.VisaIOError as e:
print(f"SCPI命令 '{command}' 执行失败: {e}")
raise
这个函数体现了文档字符串(docstring)、异常处理、参数默认值、类型注解的综合运用。后续所有仪器操作都可以基于此函数扩展。
常见误区与复盘
- 误区1:函数内修改全局变量而不声明
global,导致UnboundLocalError。 - 误区2:返回
None与返回False混淆。调用方应使用is None判断。 - 误区3:函数过长(超过50行)。应拆分为多个小函数,每个函数只做一件事。
2.4 模块与包管理:组织工程化测试代码
核心原理:模块化是代码复用的高级形式
模块(Module)是单个.py文件,包(Package)是包含__init__.py的目录。通过import语句,我们可以复用其他文件中的函数和类。在自动化测试项目中,合理的模块划分能够实现“配置与逻辑分离”“设备驱动与测试用例分离”。包管理工具pip用于安装第三方库(如pyvisa、numpy、pyserial),虚拟环境venv用于隔离项目依赖。
操作流程:创建可维护的测试项目结构
project/
├── config/
│ └── settings.py # 仪器地址、测试参数
├── drivers/
│ ├── __init__.py
│ ├── dmm.py # 万用表驱动
│ └── power_supply.py # 电源驱动
├── tests/
│ ├── __init__.py
│ ├── test_voltage.py # 电压测试用例
│ └── test_current.py # 电流测试用例
├── utils/
│ ├── __init__.py
│ ├── logger.py # 日志模块
│ └── data_parser.py # 数据解析模块
├── requirements.txt # 依赖清单
└── main.py # 测试入口
这种结构将配置、驱动、测试逻辑、工具函数分离,每个模块职责清晰。使用from config.settings import INSTRUMENT_ADDR导入配置,使用from drivers.dmm import DMM导入驱动类。
案例拆解:使用虚拟环境管理依赖
假设我们需要在项目中同时使用pyvisa(仪器控制)和pandas(数据处理)。正确的操作流程是:
# 1. 创建虚拟环境
python -m venv venv
# 2. 激活虚拟环境(Windows)
venv\Scripts\activate
# 3. 安装依赖
pip install pyvisa pandas
# 4. 导出依赖清单
pip freeze > requirements.txt
# 5. 其他开发者复现环境
pip install -r requirements.txt
虚拟环境避免了不同项目间的依赖冲突。例如,项目A需要numpy 1.21,项目B需要numpy 1.26,使用虚拟环境可以互不干扰。
常见误区与复盘
- 误区1:在模块顶层执行耗时操作。导入模块时会执行顶层代码,应将其放入
if __name__ == "__main__":块中。 - 误区2:循环导入。模块A导入模块B,模块B又导入模块A,会导致
ImportError。应重构公共代码到第三个模块。 - 误区3:忽略
requirements.txt。直接使用pip install package而不记录版本,导致环境不可复现。
from module import *。这会污染命名空间,且无法明确代码来源。应显式导入所需函数或类。
2.5 综合实战:构建一个简单的数据采集脚本
为了将上述知识串联起来,我们设计一个完整的场景:使用虚拟仪器(或模拟数据)采集三个通道的电压,判断是否在合格范围内,并将结果记录到日志文件。这个案例综合运用了数据类型、控制流、函数和模块化设计。
# main.py
import logging
from utils.data_parser import parse_voltage
from utils.logger import setup_logger
def main():
logger = setup_logger()
# 模拟仪器数据
raw_data = ["CH1:1.2345V", "CH2:0.9876V", "CH3:2.3456V"]
voltages = parse_voltage(raw_data)
for ch, v in voltages.items():
if 0.5 <= v <= 2.5:
logger.info(f"{ch} 电压 {v:.4f}V 合格")
else:
logger.warning(f"{ch} 电压 {v:.4f}V 超限")
avg = sum(voltages.values()) / len(voltages)
logger.info(f"平均电压: {avg:.4f}V")
if __name__ == "__main__":
main()
对应的data_parser.py模块:
def parse_voltage(raw_data):
"""将原始字符串列表解析为字典"""
result = {}
for item in raw_data:
ch, val = item.split(":")
result[ch] = float(val.rstrip("V"))
return result
这个案例展示了:if __name__ == "__main__"保护、模块间函数调用、字典遍历、条件判断、日志记录。整个脚本结构清晰,易于扩展——如果需要增加通道,只需修改raw_data;如果需要改变判断标准,只需修改条件表达式。