4. Python基础回顾(下):面向对象编程、装饰器、生成器与异常处理

好,咱们接着聊。上一节我们把Python的基础语法和数据结构过了一遍,这一节要啃的,是真正能让你的代码「上档次」的东西。说白了,就是让你从「写脚本」进化到「写系统」的关键技能。

在边缘AI这个领域,代码质量直接决定了你的模型能不能稳定跑在设备上。我见过太多人,模型训练得挺好,一到部署就崩,十有八九是代码结构出了问题。所以,这一节的内容,请你务必吃透。

4.1 面向对象编程:把代码「封装」起来

面向对象编程,OOP,这三个字你肯定听过。但很多人学完还是只会写class,然后里面塞一堆函数,跟写模块没啥区别。这不对。

OOP的核心思想,就三个:封装、继承、多态。 咱们一个一个说。

4.1.1 封装:把数据和操作绑在一起

想象一下,你有一个传感器,它有个ID,有个当前温度值,还有个校准系数。如果你用普通变量和函数,你得这么写:

sensor_id = "temp_01"
temperature = 25.3
calibration = 1.02

def read_temperature(sensor_id):
    # 假装去读硬件
    return 25.3 * calibration

这有什么问题?问题大了。如果我有100个传感器,我得维护100组变量。而且,谁都能改你的`calibration`,一不小心就改错了。

用类封装一下:

class TemperatureSensor:
    def __init__(self, sensor_id, calibration=1.0):
        self.sensor_id = sensor_id
        self._calibration = calibration  # 下划线表示「别乱动我」
        self._temperature = 0.0

    def read(self):
        # 模拟读取硬件
        raw_value = 25.3
        self._temperature = raw_value * self._calibration
        return self._temperature

    def set_calibration(self, value):
        if 0.5 <= value <= 2.0:
            self._calibration = value
        else:
            raise ValueError("校准系数超出范围")

你看,数据和操作绑在一起了。每个传感器对象都有自己的ID、温度和校准系数。你想改校准系数?得通过`set_calibration`方法,我还能在里面加个校验。这就是封装的好处。

我的习惯: 在嵌入式项目中,我习惯把所有硬件抽象成类。一个GPIO引脚是一个类,一个I2C设备是一个类。这样,换硬件平台的时候,只需要改底层类的实现,上层代码几乎不用动。

4.1.2 继承:站在巨人的肩膀上

继承,说白了就是「子承父业」。你有一个通用的传感器类,然后派生出温度传感器、湿度传感器、气压传感器。它们都共享一些公共属性和方法,但又各有特色。

class Sensor:
    def __init__(self, name, pin):
        self.name = name
        self.pin = pin
        self._value = 0.0

    def read(self):
        raise NotImplementedError("子类必须实现这个方法")

    def get_value(self):
        return self._value


class TemperatureSensor(Sensor):
    def __init__(self, name, pin, calibration=1.0):
        super().__init__(name, pin)  # 调用父类的初始化
        self.calibration = calibration

    def read(self):
        # 温度传感器的具体读取逻辑
        raw = 25.3
        self._value = raw * self.calibration
        return self._value


class HumiditySensor(Sensor):
    def __init__(self, name, pin, offset=0.0):
        super().__init__(name, pin)
        self.offset = offset

    def read(self):
        raw = 60.5
        self._value = raw + self.offset
        return self._value

这里有个关键点:super().__init__()。很多新手会忘记调用父类的初始化方法,导致父类的属性没被设置。我曾经排查过一个bug,查了两小时,最后发现就是忘了写这一行。

避坑指南: 我曾经在写一个多传感器融合的代码时,子类重写了父类的`read`方法,但忘了调用父类的`read`来做一些公共的预处理。结果数据一直不对。记住:重写方法时,想清楚要不要先调用父类的版本。

4.1.3 多态:同一个接口,不同行为

多态,听起来玄乎,其实很简单。就是「同样的方法名,不同的对象做不同的事」。你看上面的例子,`TemperatureSensor`和`HumiditySensor`都有`read`方法,但实现完全不同。这就是多态。

在实际项目中,多态最大的好处是:你可以写一个通用的处理函数,不用关心具体是哪种传感器。

def collect_all_sensors(sensors):
    data = {}
    for sensor in sensors:
        data[sensor.name] = sensor.read()  # 管你是什么传感器,调用read就行
    return data

# 使用
sensors = [
    TemperatureSensor("temp1", 1),
    HumiditySensor("hum1", 2)
]
result = collect_all_sensors(sensors)

你想想看,如果以后要加一个气压传感器,我只需要写一个`PressureSensor`类,继承`Sensor`,实现`read`方法。`collect_all_sensors`函数一行代码都不用改。这就是面向对象设计的威力。

4.2 装饰器:给函数「加个buff」

装饰器,是Python里一个非常优雅的特性。它允许你在不修改函数本身的情况下,给函数增加额外的功能。在边缘AI中,最常见的用途就是——模型加载缓存

你想想,一个模型文件可能几十MB甚至上百MB。每次调用推理函数都重新加载一次?那性能就完蛋了。我们需要一个机制:第一次加载模型,之后直接从缓存取。

用装饰器来实现,干净利落:

import functools

def cache_model(func):
    """装饰器:缓存模型加载结果"""
    _model_cache = {}

    @functools.wraps(func)
    def wrapper(model_path, *args, **kwargs):
        if model_path not in _model_cache:
            print(f"首次加载模型:{model_path}")
            _model_cache[model_path] = func(model_path, *args, **kwargs)
        else:
            print(f"从缓存获取模型:{model_path}")
        return _model_cache[model_path]

    return wrapper


@cache_model
def load_model(model_path):
    """模拟加载一个大型模型"""
    # 这里假设是加载TensorFlow Lite或ONNX模型
    print(f"正在从{model_path}加载模型...")
    return {"model": "模拟模型对象", "path": model_path}


# 测试
model1 = load_model("model.tflite")  # 首次加载
model2 = load_model("model.tflite")  # 从缓存获取
print(model1 is model2)  # True,同一个对象

你看,`@cache_model`这个装饰器,就像给`load_model`函数加了一个「记忆」功能。第一次调用时,它真的去加载模型,并把结果存到字典里。第二次调用时,直接返回缓存的结果。

关键点: 装饰器的本质是一个函数,它接收一个函数作为参数,返回一个新的函数。上面的`@cache_model`等价于`load_model = cache_model(load_model)`。

我个人习惯,在边缘设备上,模型加载的缓存一定要做。因为设备的RAM和Flash都很有限,反复加载模型不仅慢,还容易导致内存碎片。用装饰器,代码既简洁又安全。

4.3 生成器:数据流的「水管」

生成器,是Python里处理数据流的利器。在边缘AI中,数据往往是源源不断进来的——摄像头一帧一帧的图像,麦克风一段一段的音频,传感器一个一个的读数。如果你一次性把所有数据都加载到内存里,那设备很快就崩了。

生成器就是解决这个问题的。它像一个「水管」,数据是一滴一滴流过来的,你处理一滴,它就产生下一滴。内存占用几乎不变。

def sensor_data_stream(sensor, num_samples=100):
    """生成器:模拟传感器数据流"""
    for i in range(num_samples):
        # 模拟读取传感器
        data = sensor.read()
        yield data  # 关键:yield让这个函数变成生成器
    print("数据流结束")


# 使用
temp_sensor = TemperatureSensor("temp1", 1)
for data in sensor_data_stream(temp_sensor, 10):
    # 处理每一帧数据
    print(f"处理数据:{data}")
    # 这里可以做推理、滤波、存储等操作

注意那个`yield`关键字。它和`return`很像,但区别在于:yield不会终止函数,它只是「暂停」函数,把值返回给调用者。下次再调用生成器时,函数从暂停的地方继续执行。

在实际项目中,我经常把生成器和数据预处理管道结合起来:

def preprocess_pipeline(data_stream):
    """数据预处理管道:也是一个生成器"""
    for raw_data in data_stream:
        # 做一些预处理
        processed = raw_data * 1.02  # 校准
        yield processed


def inference_pipeline(processed_stream, model):
    """推理管道:还是一个生成器"""
    for data in processed_stream:
        result = model.predict(data)
        yield result


# 串联起来
raw_stream = sensor_data_stream(temp_sensor, 100)
processed_stream = preprocess_pipeline(raw_stream)
result_stream = inference_pipeline(processed_stream, model)

for result in result_stream:
    print(f"推理结果:{result}")

你看,三个生成器串联起来,数据就像流水线上的产品一样,经过一道道工序。每个环节只处理当前的数据,内存占用极低。这就是生成器的精髓。

我的经验: 在树莓派或Jetson Nano这类设备上,内存通常只有1-4GB。如果用列表一次性加载10000张图片,内存直接爆掉。用生成器,一次只处理一张图片,稳得很。

4.4 异常处理:让程序「摔倒了还能爬起来」

边缘设备的环境很恶劣。电压不稳、传感器断连、文件系统损坏……什么都有可能发生。如果你的程序一遇到错误就直接崩溃,那这个产品就没法用。

异常处理,就是让你的程序在遇到错误时,能优雅地处理,而不是直接「躺平」。

Python的异常处理结构很简单:

try:
    # 可能出错的代码
    result = risky_operation()
except ValueError as e:
    # 处理特定类型的异常
    print(f"值错误:{e}")
    result = None
except IOError as e:
    # 处理IO错误
    print(f"IO错误:{e}")
    # 尝试重新连接设备
    reconnect_device()
except Exception as e:
    # 捕获所有其他异常(慎用)
    print(f"未知错误:{e}")
    log_error(e)
else:
    # 没有异常时执行
    print("操作成功")
finally:
    # 无论是否异常都执行
    print("清理资源")

在边缘AI中,最常见的场景是模型推理失败。比如,输入数据格式不对,或者模型文件损坏。

def safe_inference(model, input_data):
    """安全的推理函数"""
    try:
        # 检查输入数据
        if not isinstance(input_data, np.ndarray):
            raise TypeError("输入必须是numpy数组")

        # 执行推理
        result = model.predict(input_data)
        return result

    except TypeError as e:
        print(f"输入类型错误:{e}")
        return None
    except ValueError as e:
        print(f"输入值错误:{e}")
        return None
    except Exception as e:
        # 记录错误到日志文件
        with open("error.log", "a") as f:
            f.write(f"{datetime.now()}: {e}\n")
        print("推理失败,已记录日志")
        return None
避坑指南: 我曾经犯过一个错误——在`except`子句里直接`pass`。结果程序不报错了,但也不工作了。数据全丢了,我还不知道。记住:永远不要空着except。至少打印一条日志,或者返回一个默认值。

还有一个常见的坑:异常处理的粒度。有些人喜欢把整个程序包在一个大`try...except`里,这样虽然不会崩溃,但出了问题你根本不知道是哪里出的。我建议,在关键操作(如文件读写、网络请求、模型推理)周围加细粒度的异常处理,其他地方让异常自然传播。

嗯,到这里,Python基础回顾就结束了。这一节的内容,说实话,比上一节「硬」得多。但这些都是你在实际项目中每天都会用到的东西。尤其是面向对象和异常处理,在边缘AI的部署代码中,几乎是标配。

下一节,我们就要开始真正接触AI模型了。准备好了吗?