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」,但没人知道是哪行代码出的问题。后来我重构了一遍,把所有异常都加上了上下文和堆栈信息,问题定位快了十倍。
好了,这一章就到这里。面向对象帮你组织代码,装饰器帮你复用逻辑,上下文管理器帮你管好资源,异常处理帮你稳住脚本。这四个东西组合起来,你写的脚本就不再是「一次性玩具」,而是能跑在生产环境里的工具了。