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`方法,我还能在里面加个校验。这就是封装的好处。
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,查了两小时,最后发现就是忘了写这一行。
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`函数加了一个「记忆」功能。第一次调用时,它真的去加载模型,并把结果存到字典里。第二次调用时,直接返回缓存的结果。
我个人习惯,在边缘设备上,模型加载的缓存一定要做。因为设备的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}")
你看,三个生成器串联起来,数据就像流水线上的产品一样,经过一道道工序。每个环节只处理当前的数据,内存占用极低。这就是生成器的精髓。
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
还有一个常见的坑:异常处理的粒度。有些人喜欢把整个程序包在一个大`try...except`里,这样虽然不会崩溃,但出了问题你根本不知道是哪里出的。我建议,在关键操作(如文件读写、网络请求、模型推理)周围加细粒度的异常处理,其他地方让异常自然传播。
嗯,到这里,Python基础回顾就结束了。这一节的内容,说实话,比上一节「硬」得多。但这些都是你在实际项目中每天都会用到的东西。尤其是面向对象和异常处理,在边缘AI的部署代码中,几乎是标配。
下一节,我们就要开始真正接触AI模型了。准备好了吗?