4、Python基础回顾(下):面向对象编程(类与继承)、装饰器与上下文管理器、文件I/O操作、正则表达式入门
好,咱们接着聊。上一节我们把Python的基础语法和数据结构过了一遍,这一节要啃的,是真正让Python变得强大的几个核心特性。说实话,这些内容在自动化测试框架里几乎天天用。你写不好类,框架就搭不起来;用不好装饰器,代码就冗余得没法看;文件I/O搞不定,测试数据就喂不进去;正则表达式不会,日志分析就得靠肉眼。嗯,咱们一个一个来。
4.1 面向对象编程:类与继承
面向对象编程,说白了就是把数据和操作数据的方法打包在一起。我刚开始做测试脚本时,全是面向过程,一个脚本几百行,改一个功能得翻半天。后来用了类,整个世界清爽了。
4.1.1 类的定义与实例化
先看一个最简单的类。在SIL测试里,我们经常要模拟一个ECU(电子控制单元)的行为。
class ECU:
"""模拟一个电子控制单元"""
def __init__(self, ecu_id, protocol):
self.ecu_id = ecu_id # 实例属性
self.protocol = protocol # 实例属性
self._voltage = 12.0 # 私有属性,习惯加下划线
def power_on(self):
print(f"ECU {self.ecu_id} 上电,电压 {self._voltage}V")
def get_voltage(self):
return self._voltage
# 实例化
ecu1 = ECU("ABS_01", "CAN")
ecu1.power_on()
这里有个小细节:__init__ 是构造方法,创建对象时自动调用。self 代表实例本身,必须作为第一个参数。我个人习惯把私有属性用单下划线开头,虽然Python没有真正的私有,但这是一种约定。
self,结果方法调用时一直报参数数量错误。排查了半小时才发现——嗯,这种低级错误,犯过一次就记住了。
4.1.2 继承与多态
继承是面向对象的核心。在自动化测试里,我们经常有一个基类,然后派生出各种具体的测试设备类。
class CANECU(ECU): # 继承自ECU
def __init__(self, ecu_id, can_channel):
super().__init__(ecu_id, "CAN") # 调用父类构造
self.can_channel = can_channel
def send_message(self, msg_id, data):
print(f"通过CAN通道{self.can_channel}发送消息: ID=0x{msg_id:X}")
class LINECU(ECU): # 另一个子类
def __init__(self, ecu_id, lin_id):
super().__init__(ecu_id, "LIN")
self.lin_id = lin_id
def send_message(self, msg_id, data):
print(f"通过LIN ID={self.lin_id}发送消息")
# 多态:同一个接口,不同实现
ecus = [CANECU("ABS_01", 0), LINECU("BCM_01", 1)]
for ecu in ecus:
ecu.send_message(0x100, [0x01, 0x02])
你想想看,如果没有多态,你得写一堆if-else来判断设备类型。有了继承和多态,代码扩展起来就轻松多了。加一个新协议?直接继承ECU,实现自己的send_message就行。
super() 用来调用父类方法,这在重写 __init__ 时几乎是必须的。另外,Python支持多重继承,但我建议慎用——我在项目中见过多重继承导致的混乱,调试起来非常痛苦。
4.2 装饰器与上下文管理器
这两个东西,初看有点玄乎,用熟了就会发现它们是真香。
4.2.1 装饰器:给函数加点料
装饰器本质上是一个函数,它接收一个函数作为参数,返回一个新的函数。说白了,就是在不修改原函数代码的前提下,给它增加功能。
在SIL测试里,我经常用装饰器来做测试用例的日志记录和异常处理。
import functools
import time
def log_execution(func):
"""记录函数执行时间和结果"""
@functools.wraps(func) # 保留原函数信息
def wrapper(*args, **kwargs):
start = time.time()
print(f"开始执行: {func.__name__}")
try:
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"执行完成: {func.__name__}, 耗时{elapsed:.3f}s")
return result
except Exception as e:
print(f"执行失败: {func.__name__}, 错误: {e}")
raise
return wrapper
@log_execution
def test_ecu_communication():
"""测试ECU通信"""
time.sleep(0.5) # 模拟测试耗时
return True
test_ecu_communication()
这里有个坑:如果不加 @functools.wraps,被装饰函数的 __name__ 和 __doc__ 会变成wrapper的。我曾经因为这个在生成测试报告时,函数名全变成了"wrapper",排查了半天。
4.2.2 上下文管理器:with语句的魔法
上下文管理器最常见的用法就是 with open(...) as f。它的核心是 __enter__ 和 __exit__ 两个方法。在测试中,我常用它来管理硬件资源。
class CANBus:
"""模拟CAN总线连接,使用上下文管理器自动释放资源"""
def __init__(self, channel):
self.channel = channel
def __enter__(self):
print(f"打开CAN通道 {self.channel}")
# 这里可以执行硬件初始化
return self # 返回给as后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"关闭CAN通道 {self.channel}")
# 这里执行资源清理
if exc_type:
print(f"发生异常: {exc_val}")
return False # 返回False表示不抑制异常
# 使用
with CANBus(0) as bus:
print(f"正在使用通道 {bus.channel} 进行通信")
# 即使这里发生异常,__exit__也会被调用
__exit__ 返回 True 会抑制异常,即异常不会被抛出。除非你有特殊需求,否则建议返回 False,让异常正常传播。我曾经为了图省事返回了True,结果异常被吞掉了,调试时完全找不到问题根源。
4.3 文件I/O操作
文件操作是测试脚本的日常。读取测试用例、写入测试报告、解析配置文件,哪样都离不开它。
4.3.1 文本文件读写
最基础的操作,但细节决定成败。
# 写入文件
with open("test_result.txt", "w", encoding="utf-8") as f:
f.write("测试用例ID: TC001\n")
f.write("测试结果: PASS\n")
f.writelines(["耗时: 1.2s\n", "备注: 无异常\n"])
# 读取文件
with open("test_result.txt", "r", encoding="utf-8") as f:
# 方法1:全部读取
content = f.read()
# 方法2:逐行读取(推荐大文件使用)
for line in f:
print(line.strip())
我个人习惯总是指定 encoding="utf-8"。为什么?因为我在项目中遇到过,脚本在Windows上跑得好好的,部署到Linux服务器上就乱码了——就是因为没指定编码。
4.3.2 二进制文件与CSV操作
在SIL测试中,我们经常要处理二进制数据,比如刷写ECU的固件文件。
# 二进制文件读写
with open("firmware.bin", "rb") as f:
header = f.read(16) # 读取前16字节
data = f.read() # 读取剩余内容
# CSV文件操作
import csv
# 写入CSV
with open("test_cases.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["ID", "名称", "预期结果"])
writer.writerows([
["TC001", "上电测试", "PASS"],
["TC002", "通信测试", "PASS"]
])
# 读取CSV
with open("test_cases.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["ID"], row["名称"])
newline="",否则在Windows上每行后面会多一个空行。这个坑我踩过,现在写CSV时已经形成肌肉记忆了。
4.4 正则表达式入门
正则表达式,很多人觉得难,其实掌握几个核心模式就够了。在测试中,我主要用它来解析日志、提取数据、验证格式。
4.4.1 基础模式与常用函数
import re
# 常用函数
text = "ECU ID: ABS_01, 电压: 12.5V, 温度: 85°C"
# re.search: 查找第一个匹配
match = re.search(r"电压:\s*([\d.]+)V", text)
if match:
voltage = match.group(1) # 提取电压值
print(f"电压值: {voltage}")
# re.findall: 查找所有匹配
ids = re.findall(r"([A-Z]+_\d+)", text)
print(f"找到的ID: {ids}")
# re.sub: 替换
new_text = re.sub(r"\d+\.\d+V", "12.0V", text)
print(f"替换后: {new_text}")
# re.match: 从开头匹配
if re.match(r"ECU", text):
print("文本以ECU开头")
4.4.2 实战:解析测试日志
我给大家看一个我在项目中实际用过的例子。解析CANoe的测试日志,提取测试结果。
log_text = """
[2024-01-15 10:30:01] Test Case: TC001 - 上电测试
[2024-01-15 10:30:02] Status: PASS
[2024-01-15 10:30:03] Duration: 1.234s
[2024-01-15 10:30:04] Test Case: TC002 - 通信测试
[2024-01-15 10:30:05] Status: FAIL
[2024-01-15 10:30:06] Duration: 0.567s
"""
pattern = r"Test Case: (\w+) - (.+?)\n.*?Status: (\w+)\n.*?Duration: ([\d.]+)s"
matches = re.findall(pattern, log_text, re.DOTALL)
for tc_id, tc_name, status, duration in matches:
print(f"{tc_id}: {tc_name} -> {status} ({duration}s)")
| 模式 | 含义 | 示例 |
|---|---|---|
. | 匹配任意字符(除换行) | a.b 匹配 "acb" |
* | 重复0次或多次 | ab* 匹配 "a", "ab", "abb" |
+ | 重复1次或多次 | ab+ 匹配 "ab", "abb" |
? | 重复0次或1次 | ab? 匹配 "a", "ab" |
\d | 匹配数字 | \d+ 匹配 "123" |
\w | 匹配字母、数字、下划线 | \w+ 匹配 "TC001" |
[] | 字符集 | [A-Z]+ 匹配大写字母 |
() | 分组捕获 | (\d+)V 提取电压值 |
. 默认不匹配换行符。如果你要跨行匹配,记得加上 re.DOTALL 标志。我曾经在解析多行日志时,怎么都匹配不到,最后发现是忘了这个标志。
好了,这一节的内容就到这里。面向对象、装饰器、文件操作、正则表达式,这四个东西在SIL测试自动化里是基本功。你写测试脚本时,多想想能不能用类来组织代码,能不能用装饰器来复用逻辑,能不能用正则来解析数据。嗯,慢慢练,熟能生巧。