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["名称"])
小技巧: 写CSV时记得加 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测试自动化里是基本功。你写测试脚本时,多想想能不能用类来组织代码,能不能用装饰器来复用逻辑,能不能用正则来解析数据。嗯,慢慢练,熟能生巧。

Python基础回顾(下)知识体系 面向对象编程 类与实例 继承与多态 封装与私有属性 super() 调用父类 装饰器 & 上下文 装饰器:@语法糖 functools.wraps __enter__ / __exit__ with 语句管理资源 文件 I/O 操作 文本文件读写 二进制文件操作 CSV 文件处理 编码指定 (utf-8) 正则表达式 re.search / re.match re.findall / re.sub 分组捕获 () 常用模式速查 SIL测试自动化框架核心基础

专注资料整理