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去读,直接乱码。

注意:读取DBC或ARXML文件时,建议显式指定编码格式:
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_messagereceive_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,重写setupexecuteteardown三个方法就行。测试框架的骨架搭好了,剩下的就是填肉。


好了,这一章的内容就到这。文件读写让你能跟数据打交道,异常处理让你的脚本更健壮,面向对象编程让你的代码更优雅。这三样东西,在车载网络自动化测试中缺一不可。

下一章我们会把这些知识串起来,写一个完整的自动化测试脚本。到时候你就知道,今天学的这些东西到底有多实用了。