3、Python基础回顾:面向对象编程(类与继承)、装饰器模式、上下文管理器(with语句)、异常处理最佳实践

说实话,很多做车载网络的朋友一开始都问我:「我一个搞CAN/LIN的,学Python面向对象干嘛?」

我的回答很简单——你写脚本的时候,是不是经常复制粘贴一大段代码?是不是改一个协议就得改好几个函数?如果是,那你需要面向对象。

这一章,我带你快速过一遍Python里最实用的几个概念。不扯虚的,全是干活用得上的。

3.1 类与继承:把CAN报文封装成对象

先说说类。我习惯把类理解成一个「模板」。比如CAN报文,它有ID、DLC、数据域、时间戳。你每次处理一条报文,难道都要写一个字典?

用类封装一下,清爽多了。

class CanMessage:
    def __init__(self, can_id, dlc, data, timestamp=None):
        self.can_id = can_id
        self.dlc = dlc
        self.data = data
        self.timestamp = timestamp

    def is_extended(self):
        return self.can_id & 0x80000000 != 0

    def __str__(self):
        return f"ID:0x{self.can_id:X} DLC:{self.dlc} Data:{self.data.hex()}"

你看,一个报文对象,自带判断扩展帧的方法,打印也方便。我在项目里经常用这种结构来统一管理报文流。

再说继承。车载协议那么多,CAN、CAN FD、LIN……但它们有很多共性。我建议你写一个基类,然后派生出具体协议类。

class BaseMessage:
    def __init__(self, bus_type):
        self.bus_type = bus_type

    def encode(self):
        raise NotImplementedError

class CanMessage(BaseMessage):
    def __init__(self, can_id, data):
        super().__init__('CAN')
        self.can_id = can_id
        self.data = data

    def encode(self):
        # 具体编码逻辑
        pass
我的习惯:基类里把公共方法定义好,子类只实现差异部分。这样加一个新协议,你只需要写一个子类,不用动老代码。

3.2 装饰器模式:给函数加点「料」

装饰器是什么?说白了,就是一个函数,用来修改另一个函数的行为。不改变原函数代码,却能给它加功能。

我在写日志记录时特别喜欢用装饰器。比如每条报文收发都要打日志,难道每个函数里都写一遍print

def log_message(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] 调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[LOG] 函数返回: {result}")
        return result
    return wrapper

@log_message
def send_can_message(msg):
    # 实际发送逻辑
    return "发送成功"

你想想看,只要在函数头上加个@log_message,日志功能就有了。我曾经用装饰器统一处理了所有诊断服务的超时重试逻辑,代码量减少了一半。

注意:装饰器会改变函数的元信息(比如__name__)。建议用functools.wraps来保留原函数信息。

3.3 上下文管理器:用with语句管好资源

做车载网络调试,经常要打开CAN设备、文件、socket连接。用完不关?轻则资源泄漏,重则设备锁死。

Python的with语句就是干这个的。它保证无论代码是否报错,资源都会被正确释放。

with open("can_log.txt", "r") as f:
    data = f.read()
# 文件自动关闭,不用你操心

但如果你要管理的是CAN设备呢?自己写一个上下文管理器也很简单。

class CanDevice:
    def __enter__(self):
        print("打开CAN设备")
        self.device = open_device()
        return self.device

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("关闭CAN设备")
        self.device.close()

# 使用
with CanDevice() as dev:
    dev.send(CanMessage(0x100, 8, b'\x00'*8))

嗯,这里要注意:__exit__方法里的三个参数分别处理异常类型、值和回溯。如果你返回True,异常会被吞掉。我个人建议不要轻易吞异常,除非你明确知道自己在做什么。

3.4 异常处理最佳实践:别让脚本崩在半夜

我记得有一次,凌晨两点被电话叫醒——自动化脚本崩了,原因是CAN总线上一帧超时。从那以后,我对异常处理格外上心。

异常处理不是简单写个try...except就完事了。有几个原则我一直在用:

  • 精确捕获异常类型:别用except:裸捕获,会连键盘中断都吃掉。
  • 保留异常上下文:用raise ... from ...保留原始异常链。
  • 该抛就抛:有些异常你处理不了,不如让上层去处理。
def read_can_message(device):
    try:
        msg = device.receive(timeout=1.0)
        if msg is None:
            raise TimeoutError("CAN接收超时")
        return msg
    except TimeoutError as e:
        # 记录日志,重新尝试
        logger.warning(f"超时: {e}")
        raise  # 重新抛出,让调用者决定
    except DeviceError as e:
        # 设备错误,需要重置
        logger.error(f"设备异常: {e}")
        device.reset()
        raise RuntimeError("CAN设备异常") from e

最佳实践总结:

  • 永远不要用except: pass——这是最坑的写法
  • 日志要记录异常堆栈:logger.exception("xxx")
  • 自定义异常类,让错误语义更清晰
  • finally块里只做资源清理,别写业务逻辑

我曾经接手过一个脚本,里面全是except Exception as e: print(e)。结果呢?脚本跑了一周,日志里全是「NoneType has no attribute」,但没人知道是哪行代码出的问题。后来我重构了一遍,把所有异常都加上了上下文和堆栈信息,问题定位快了十倍。

好了,这一章就到这里。面向对象帮你组织代码,装饰器帮你复用逻辑,上下文管理器帮你管好资源,异常处理帮你稳住脚本。这四个东西组合起来,你写的脚本就不再是「一次性玩具」,而是能跑在生产环境里的工具了。