2、Python基础回顾:数据类型、控制流、函数、模块与包管理

自动化测试与仪器控制的本质,是使用程序精确地描述“读取数据—做出判断—执行动作—记录结果”这一闭环。Python之所以成为该领域的首选语言,并非因为其语法最简单,而是因为其数据类型能够自然映射仪器返回的数值与字符串、控制流能够模拟复杂的时序逻辑、函数与模块机制则让测试代码具备可维护性和复用性。本章将系统梳理这五个核心主题,并始终围绕“为后续编写自动化脚本打基础”这一目标展开。我们不仅会回顾语法,更会从工程视角审视:如何选择合适的数据结构、如何设计控制逻辑、如何组织代码文件,从而避免在后续章节中因基础不牢而陷入调试泥潭。

Python基础回顾:知识框架与工程映射 Python 核心基础 数据类型 数值 / 字符串 / 列表 / 元组 / 字典 / 集合 控制流 if / for / while / break / continue 函数 参数 / 返回值 / 作用域 / 匿名函数 模块与包管理 import / pip / venv / 项目结构 自动化脚本工程映射 仪器读数 → 数据类型解析 → 控制流判断 → 函数封装 → 模块化组织 异常处理与日志记录贯穿始终,保证脚本健壮性与可追溯性

上图展示了本章的知识框架。数据类型是描述仪器数据的“语言”,控制流是脚本的“决策中枢”,函数是“逻辑封装单元”,模块与包管理则是“工程组织方式”。四个部分环环相扣,最终统一服务于自动化测试脚本的健壮性、可读性与可维护性。下面我们逐一深入。

2.1 数据类型:精准描述仪器世界的基石

核心原理:数据类型的本质是内存布局与操作语义

Python中的每个变量都是对象的引用,而对象由其类型决定内存占用和可用操作。在仪器控制场景中,我们最常打交道的类型包括:int(整数,如计数器读数)、float(浮点数,如电压值)、str(字符串,如仪器返回的SCPI命令响应)、list(列表,如扫描得到的波形数据点)、tuple(元组,如固定通道配置)、dict(字典,如通道号到物理量的映射)、set(集合,用于去重或成员判断)。理解可变与不可变对象至关重要:list、dict、set可变,而int、float、str、tuple不可变。这直接影响函数传参时的行为——可变对象在函数内修改会影响到外部变量。

判断方法:根据数据特征选择合适容器

面对一组仪器数据,选择哪种容器?可遵循以下判断路径:

  1. 是否需要保持顺序? 是→用list或tuple;否→考虑set或dict。
  2. 是否需要按键访问? 是→用dict,键可以是通道名或参数名。
  3. 数据是否允许重复? 不允许→用set;允许→用list。
  4. 数据是否会被修改? 不会→用tuple,既安全又节省内存。
经验提醒:在解析仪器返回的CSV或二进制数据时,优先使用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中断)时执行。

操作流程:设计健壮的循环结构

  1. 明确循环终止条件:对于while循环,务必设置最大迭代次数或超时时间,防止死循环。
  2. 优先使用for而非while:当迭代次数已知时,for更简洁且不易出错。
  3. 利用enumerate获取索引:需要同时获取元素和索引时,使用for i, val in enumerate(data)。
  4. 使用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)是避免变量污染的关键。

设计方法:编写高质量测试函数

  1. 单一职责原则:一个函数只做一件事。例如read_voltage(channel)不应同时负责切换通道和记录日志。
  2. 参数显式化:避免使用全局变量传递数据,通过参数传入。
  3. 返回值明确:成功返回数据,失败返回None或抛出异常,不要混合返回类型。
  4. 使用类型注解: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;如果需要改变判断标准,只需修改条件表达式。

总结复盘:本章回顾的五个主题构成了自动化测试脚本的骨架。数据类型是“血肉”,控制流是“神经”,函数是“肌肉”,模块与包管理是“骨骼”。在实际项目中,建议遵循以下原则:优先使用内置数据类型和标准库;函数保持短小精悍;模块划分遵循高内聚低耦合;始终使用虚拟环境管理依赖。下一章将在此基础上,深入探讨Python在仪器通信中的具体应用——串口与VISA协议编程。