第4章:Python面向对象编程——类、继承、多态,封装仪器驱动

为什么ATE编程离不开面向对象?

你可能刚入门时会疑惑:写个仪器控制脚本,直接调用VISA库不行吗?行是行,但规模化ATE项目里,你会发现代码膨胀到没法看。我一老同事曾把整个测试系统写成了一个几千行的单文件,后来加一台新仪器,改了三天,还引入了五个bug。嗯,那之后,我们就把面向对象搬了进来。

说白了,面向对象就是帮你把“仪器”和“操作”打包成一块块积木。每块积木内部复杂,但对外只暴露简单的接口。你换仪器,只需换积木,不用改测试流程。

类:第一块积木——把你的数字万用表封装起来

先看一个最简单的例子。我手头有一台Keysight 34465A数字万用表。我们会为它写一个类:

class DMM:
    """数字万用表驱动类"""
    def __init__(self, resource_name):
        self.resource = resource_name
        self.visa_handle = None

    def connect(self):
        # 实际连接代码略
        self.visa_handle = rm.open_resource(self.resource)

    def measure_dc_voltage(self):
        # 发送指令,返回电压值
        return self.visa_handle.query("MEAS:VOLT:DC?")

你看,_ _ init _ _ 就像给积木起名字、分配资源。connect()和measure_dc_voltage()是它能干的事。我习惯把每个仪器类放在独立文件里,比如dmm.py、oscillo.py,方便管理。

一个建议:资源字符串(如“USB0::0x2A8D::0x1301::MY58012345::INSTR”)最好从配置文件读取,别硬编码。我曾在项目里硬编码过一次,结果换了一台相同型号的仪器,序列号变了,找了好久才发现。

继承:让万用表和示波器共享一个“爹”

为什么需要继承?你想想看,所有仪器都有连接、断开、ID查询这些通用操作。如果每种仪器都写一遍connect(),那可太傻了。我们定义一个基类

class Instrument:
    def __init__(self, resource_name):
        self.resource = resource_name
        self.visa_handle = None

    def connect(self):
        self.visa_handle = rm.open_resource(self.resource)

    def disconnect(self):
        if self.visa_handle:
            self.visa_handle.close()

    def query_id(self):
        return self.visa_handle.query("*IDN?")

然后让DMM继承它:

class DMM(Instrument):
    def measure_dc_voltage(self):
        return self.visa_handle.query("MEAS:VOLT:DC?")

同样,Oscilloscope也继承Instrument,添加自己的方法。我经历过一个项目,基类写好之后,增加一台新示波器只用了半天——仅仅实现了scope特定的测量方法,其他全复用。

避坑:我曾经犯过一个错误——在子类__init__里没有调用super().__init__(),导致父类的资源属性没初始化,连不上仪器。记住:子类重写__init__时,第一件事就是调用父类的构造。

多态:换仪器不用改测试流程

多态是个高大上的词,用大白话讲就是“同样的调用方式,不同的行为”。比如我们定义一个测试函数,只需要知道仪器类有measure()方法,不管它是DMM还是PowerSupply:

def run_test(instrument):
    result = instrument.measure()   # 多态调用
    if result > 1.0:
        print("FAIL")
    else:
        print("PASS")

测试流程根本不用知道你是谁。我后来写过一个“仪器切换器”,根据配置文件里的仪器类型,动态创建对象:

def create_instrument(config):
    if config['type'] == 'DMM':
        return DMM(config['resource'])
    elif config['type'] == 'OSC':
        return Oscilloscope(config['resource'])
    # ...

跑一下run_test(create_instrument(cfg)),换仪器就像换零件,测试函数纹丝不动。是不是清爽很多?

用一张图看清这一切

下面这张图展示了基类、子类和多态调用的层次关系。你抬头看SVG,一目了然:

仪器驱动类层次结构 Instrument DMM Oscilloscope PowerSupply 多态调用:run_test(instrument) Base: Instrument (connect, disconnect, query_id) Derived: DMM, Oscilloscope, PowerSupply (各自测量方法)

封装仪器驱动的实战要点

光有理论不够。我把这几年摸爬滚打的套路列出来:

  1. 每个驱动器一个类,一个文件。别把万用表和电源放在一起,你会后悔的。
  2. 基类只放公共操作。比如连接、断开、ID查询。别过早设计虚方法,需要时再添加。
  3. 多态接口保证签名一致。比如所有仪器类都有measure()或query(),参数列表统一。
  4. 异常处理统一。我一般会在基类里定义一个raise_visa_error(),子类调用它,错误信息格式一致,调试方便。
  5. 资源路径从外部传进来。别写死,用配置文件或环境变量。

一句话总结:类把仪器功能打包,继承消除重复代码,多态让测试程序与具体仪器解耦。你在ATE项目中越早用上这三个特性,后期维护越轻松。

当然,面向对象不是银弹。如果一个仪器只有一两个操作,写个函数也行,别过度设计。我见过有人给一个开关矩阵写了6层继承,改了需求后全推翻——适度原则很重要。

好了,这一章就把Python面向对象在ATE仪器驱动中的用法讲清楚了。下去后你可以拿自己手头的仪器试试,写一个基类和一个子类,跑跑看。代码跑通了,你会更有感觉。