4、Python基础速通(二):文件读写、异常处理与面向对象编程
好,咱们接着往下聊。上一章我们把Python的基础语法和数据结构过了一遍,今天要啃的这三块——文件读写、异常处理、面向对象编程,说白了就是让你写的脚本真正能干活、扛得住、还能复用。
我在车载网络测试这行干了快十年,见过太多脚本跑着跑着就崩了,原因无非就这几样:文件没读到、数据格式不对、或者代码写得太死板。今天咱们就把这些坑一个个填上。
4.1 文件读写:open/csv/json
车载网络测试里,文件读写是家常便饭。你要解析DBC文件、读取CAN日志、保存测试报告,哪样都离不开它。
4.1.1 基础文件操作:open()
Python内置的open()函数,是操作文件的入口。我个人习惯用with语句,它会自动帮你关闭文件,省心不少。
# 读取文件
with open('can_log.txt', 'r') as f:
content = f.read()
print(content)
# 写入文件
with open('test_result.txt', 'w') as f:
f.write('PASS: CAN ID 0x123 信号正常\n')
这里有个坑,我当年刚入行时踩过——文件编码问题。车载工具链很多是德国人写的,默认用Latin-1编码。你用UTF-8去读,直接乱码。
with open('vehicle.dbc', 'r', encoding='latin-1') as f:
# 处理文件内容
4.1.2 CSV文件读写
测试数据经常以CSV格式存储,比如CANoe的日志导出、测试用例表格。Python的csv模块是标准库,直接拿来用就行。
import csv
# 读取CSV
with open('test_cases.csv', 'r') as f:
reader = csv.reader(f)
for row in reader:
print(f'测试用例ID: {row[0]}, 期望结果: {row[1]}')
# 写入CSV
with open('test_report.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['测试ID', '结果', '耗时(ms)'])
writer.writerow(['TC001', 'PASS', 12.5])
writer.writerow(['TC002', 'FAIL', 0])
嗯,这里要注意newline=''这个参数。不加的话,Windows系统会在每行后面多一个空行,看着特别别扭。我曾经因为这个被测试经理怼过,说报告格式不专业……从那以后我写CSV必加这个参数。
4.1.3 JSON文件读写
JSON格式在配置文件和API交互中很常见。比如我们做自动化测试时,会把测试参数放在JSON配置文件里,方便修改。
import json
# 读取JSON配置
with open('test_config.json', 'r') as f:
config = json.load(f)
print(f'CAN通道: {config["channel"]}')
print(f'波特率: {config["baudrate"]}')
# 将测试结果保存为JSON
test_result = {
'test_id': 'TC003',
'status': 'PASS',
'timestamp': '2024-01-15 10:30:00',
'details': {
'signal_name': 'EngineSpeed',
'value': 3000,
'unit': 'rpm'
}
}
with open('test_result.json', 'w') as f:
json.dump(test_result, f, indent=2, ensure_ascii=False)
indent=2让JSON输出更易读,ensure_ascii=False确保中文字符正常显示。这两个参数我几乎每次都用。
4.2 异常处理:try-except
车载网络测试脚本跑在真实硬件上,什么幺蛾子都可能发生。CAN总线突然断开、文件被占用、数据格式异常……如果没有异常处理,脚本直接崩溃,测试就得重来。
4.2.1 基本用法
try:
with open('can_log.txt', 'r') as f:
data = f.read()
except FileNotFoundError:
print('日志文件不存在,请检查路径')
except PermissionError:
print('没有权限读取该文件')
except Exception as e:
print(f'发生未知错误: {e}')
你想想看,如果不加这个try-except,文件一找不到,整个脚本就挂了。在自动化测试中,一个用例失败不应该影响其他用例的执行。
4.2.2 实战中的异常处理模式
我在项目中遇到过一种情况:测试脚本要连续读取1000帧CAN报文,但第500帧时总线突然断了。如果不用异常处理,前面499帧的数据全白费了。
success_count = 0
fail_count = 0
for frame_id in range(1000):
try:
result = read_can_frame(frame_id)
process_frame(result)
success_count += 1
except CANBusError as e:
print(f'第{frame_id}帧读取失败: {e}')
fail_count += 1
# 记录失败,继续下一帧
continue
except Exception as e:
print(f'严重错误: {e}')
break
print(f'测试完成: 成功{success_count}帧, 失败{fail_count}帧')
4.2.3 finally和else子句
try:
can_device = open_can_interface()
data = can_device.read()
except CANError:
print('CAN接口异常')
else:
# 没有异常时执行
print(f'成功读取数据: {data}')
finally:
# 无论是否异常都执行
can_device.close()
print('CAN接口已关闭')
我曾经犯过一个低级错误:在try块里打开了硬件接口,异常发生时忘记关闭,导致下次测试时接口被占用。后来我养成了习惯,所有资源释放都放在finally里。
4.3 面向对象编程:类与对象
面向对象编程(OOP)是Python的核心思想之一。说白了,就是把数据和操作数据的方法打包在一起。在车载网络测试中,每个CAN节点、每条报文、每个信号,都可以抽象成一个对象。
4.3.1 类的定义与实例化
class CANMessage:
"""CAN报文类"""
def __init__(self, can_id, data, dlc):
self.can_id = can_id # CAN ID
self.data = data # 数据字节
self.dlc = dlc # 数据长度
def display(self):
"""显示报文信息"""
print(f'ID: 0x{self.can_id:X}, DLC: {self.dlc}, Data: {self.data.hex()}')
# 创建对象
msg1 = CANMessage(0x123, b'\x01\x02\x03\x04', 4)
msg2 = CANMessage(0x456, b'\x0A\x0B', 2)
msg1.display()
msg2.display()
你看,这样每个CAN报文都是一个独立的对象,有自己的ID、数据和长度。比用字典或列表管理清晰多了。
4.3.2 封装与属性
封装是OOP的重要特性。我习惯把内部实现细节隐藏起来,只暴露必要的接口。
class Signal:
"""CAN信号类"""
def __init__(self, name, start_bit, length, factor=1.0, offset=0):
self._name = name
self._start_bit = start_bit
self._length = length
self._factor = factor
self._offset = offset
@property
def name(self):
return self._name
def decode(self, raw_value):
"""将原始值转换为物理值"""
physical_value = raw_value * self._factor + self._offset
return physical_value
def encode(self, physical_value):
"""将物理值转换为原始值"""
raw_value = int((physical_value - self._offset) / self._factor)
return raw_value
# 使用示例
speed_signal = Signal('EngineSpeed', 0, 16, 0.1, 0)
raw = speed_signal.encode(3000) # 将3000rpm编码为原始值
physical = speed_signal.decode(raw) # 解码回物理值
_name),通过@property提供只读访问。这样既保护了数据,又保持了接口的简洁。
4.3.3 继承与多态
车载网络协议很多,CAN、CAN FD、LIN、FlexRay……但它们有很多共性。用继承可以避免重复代码。
class NetworkNode:
"""网络节点基类"""
def __init__(self, name, node_id):
self.name = name
self.node_id = node_id
def send_message(self, msg):
raise NotImplementedError("子类必须实现此方法")
def receive_message(self):
raise NotImplementedError("子类必须实现此方法")
class CANNode(NetworkNode):
"""CAN节点"""
def __init__(self, name, node_id, baudrate=500000):
super().__init__(name, node_id)
self.baudrate = baudrate
def send_message(self, msg):
print(f'[CAN] {self.name} 发送报文: {msg}')
# 实际发送逻辑
def receive_message(self):
print(f'[CAN] {self.name} 接收报文')
# 实际接收逻辑
class LINNode(NetworkNode):
"""LIN节点"""
def __init__(self, name, node_id, baudrate=19200):
super().__init__(name, node_id)
self.baudrate = baudrate
def send_message(self, msg):
print(f'[LIN] {self.name} 发送报文: {msg}')
def receive_message(self):
print(f'[LIN] {self.name} 接收报文')
# 多态:统一接口,不同实现
nodes = [
CANNode('ECU1', 0x01),
LINNode('Sensor1', 0x02)
]
for node in nodes:
node.send_message('Hello')
node.receive_message()
你想想看,如果不用继承,每个节点类都要重复写send_message和receive_message的接口定义。用基类统一约束,代码结构清晰多了。
4.3.4 实战:一个简单的测试用例类
最后,我分享一个我在项目中常用的测试用例基类模板:
class TestCase:
"""测试用例基类"""
def __init__(self, name, priority='medium'):
self.name = name
self.priority = priority
self.status = 'pending'
self.log = []
def setup(self):
"""测试前置条件"""
raise NotImplementedError
def execute(self):
"""测试执行"""
raise NotImplementedError
def teardown(self):
"""测试后置清理"""
raise NotImplementedError
def run(self):
"""运行测试用例"""
try:
self.setup()
self.execute()
self.status = 'pass'
except Exception as e:
self.status = 'fail'
self.log.append(f'错误: {e}')
finally:
self.teardown()
return self.status
class CANSignalTest(TestCase):
"""CAN信号测试"""
def setup(self):
print(f'初始化CAN接口...')
# 打开CAN设备
def execute(self):
print(f'发送测试报文...')
# 发送并验证信号
def teardown(self):
print(f'关闭CAN接口...')
# 关闭设备
# 使用
test = CANSignalTest('EngineSpeed_Test', 'high')
result = test.run()
print(f'测试结果: {result}')
这个模板我用了好几年,每次做新项目只要继承TestCase,重写setup、execute、teardown三个方法就行。测试框架的骨架搭好了,剩下的就是填肉。
好了,这一章的内容就到这。文件读写让你能跟数据打交道,异常处理让你的脚本更健壮,面向对象编程让你的代码更优雅。这三样东西,在车载网络自动化测试中缺一不可。
下一章我们会把这些知识串起来,写一个完整的自动化测试脚本。到时候你就知道,今天学的这些东西到底有多实用了。