4. Python核心基础回顾:面向对象编程、装饰器、生成器、多线程与多进程在仿真中的应用
好,咱们进入第四章。这一章我打算带大家快速过一遍Python里几个最核心、也最实用的编程概念。你可能会问:“这些基础东西,跟自动驾驶仿真有什么关系?”
关系大了去了。说白了,整个仿真平台的骨架,就是靠这些技术撑起来的。没有面向对象,你的代码就是一盘散沙;没有多线程,仿真跑起来就像老牛拉车。咱们一个一个来。
4.1 面向对象编程:仿真世界的“积木”
我个人习惯,把面向对象编程(OOP)想象成搭积木。每个“类”就是一块积木的模具,而“对象”就是用这个模具造出来的具体积木块。
在仿真里,你想想看,一辆车、一个传感器、一条路,这些都是实实在在的“对象”。我们不可能为每一辆车都写一套独立的代码,对吧?
4.1.1 类与对象:定义你的第一辆车
我们先定义一个最简单的车辆类。我在项目中遇到过,一开始把类定义得特别复杂,什么属性都想往里塞,结果代码又臭又长。后来我学乖了,先定义最核心的。
class Vehicle:
"""一个简单的车辆模型"""
def __init__(self, vehicle_id, initial_position):
self.id = vehicle_id # 车辆ID
self.position = initial_position # 当前位置 (x, y)
self.speed = 0.0 # 当前速度
self.acceleration = 0.0 # 当前加速度
def update_state(self, dt):
"""更新车辆状态,dt是时间步长"""
self.speed += self.acceleration * dt
self.position += self.speed * dt
# 嗯,这里要注意,实际仿真中还要考虑转向、摩擦等
def __str__(self):
return f"Vehicle {self.id}: pos={self.position:.2f}, speed={self.speed:.2f}"
# 创建两个车辆对象
car1 = Vehicle("CAR_001", 0.0)
car2 = Vehicle("CAR_002", 10.0)
car1.acceleration = 2.0
car1.update_state(0.1)
print(car1) # 输出:Vehicle CAR_001: pos=0.02, speed=0.20
你看,Vehicle 就是一个类,car1 和 car2 就是对象。每个对象都有自己的 id、position 和 speed,互不干扰。
4.1.2 继承:让卡车和轿车共享代码
仿真里不可能只有一种车。有卡车、轿车、公交车。它们有很多共同点,但又有各自的特点。这时候,继承就派上用场了。
class Truck(Vehicle): # 卡车继承自Vehicle
def __init__(self, vehicle_id, initial_position, max_load):
super().__init__(vehicle_id, initial_position) # 调用父类构造器
self.max_load = max_load
self.current_load = 0
def load_cargo(self, weight):
if self.current_load + weight <= self.max_load:
self.current_load += weight
print(f"装载成功,当前载重:{self.current_load}")
else:
print("超载了!")
# 使用
my_truck = Truck("TRK_001", 50.0, 10000)
my_truck.acceleration = 1.0 # 卡车加速慢一些
my_truck.update_state(0.1)
my_truck.load_cargo(5000)
4.1.3 封装与多态:让代码更安全、更灵活
封装,就是把内部实现藏起来,只暴露必要的接口。比如,我不希望外部代码直接修改车辆的 speed,而是通过 accelerate() 和 brake() 方法来控制。
class SafeVehicle:
def __init__(self, vid):
self.__vid = vid # 私有属性,外部无法直接访问
self.__speed = 0.0
def accelerate(self, delta):
if delta > 0:
self.__speed += delta
def get_speed(self):
return self.__speed
# 多态:不同车辆可以有不同的刹车逻辑
def brake(self):
self.__speed *= 0.9 # 默认刹车效果
多态就更妙了。同样是 brake() 方法,轿车可能刹车效率高,卡车因为载重刹车距离长。你只需要在子类里重写 brake() 方法,调用的时候,Python会自动根据对象类型执行对应的方法。
4.2 装饰器:给函数“加点料”
装饰器,说白了就是一个“包装工”。它可以在不修改原函数代码的情况下,给函数增加额外的功能。这在仿真里太常用了,比如记录日志、计算耗时、权限检查。
4.2.1 一个简单的计时装饰器
我记得有一次,仿真跑得特别慢,我需要找出哪个函数最耗时。手动在每个函数前后加 time.time() 太蠢了。装饰器就是干这个的。
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时:{end - start:.4f} 秒")
return result
return wrapper
@timer_decorator
def simulate_step(dt):
# 模拟一个复杂的计算
total = 0
for i in range(1000000):
total += i
return total
simulate_step(0.01) # 输出:simulate_step 执行耗时:0.0456 秒
__name__)。如果你需要保留原函数信息,记得使用 functools.wraps。我曾经因为这个bug,调试了半天才发现函数名全变成了 wrapper。
4.2.2 带参数的装饰器
有时候,我们想给装饰器传递参数,比如日志级别。这需要再包一层。
def log_decorator(level="INFO"):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{level}] 调用函数:{func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log_decorator(level="WARNING")
def check_sensor(sensor_id):
print(f"检查传感器:{sensor_id}")
check_sensor("LIDAR_01") # 输出:[WARNING] 调用函数:check_sensor
4.3 生成器:省内存的“懒人”迭代器
仿真中经常要处理大量数据,比如一秒钟采集1000帧的传感器数据。如果全部加载到内存里,你的电脑很快就会爆掉。生成器就是解决这个问题的。
生成器不会一次性生成所有数据,而是“随用随取”,每次只生成一个值。你想想看,这多省内存。
4.3.1 用 yield 创建生成器
def sensor_data_generator(num_frames):
"""模拟生成传感器数据流"""
for i in range(num_frames):
# 模拟采集一帧数据
frame_data = {"frame_id": i, "timestamp": i * 0.1, "data": [1, 2, 3]}
yield frame_data # 关键:yield 而不是 return
# 使用生成器
data_stream = sensor_data_generator(1000)
for frame in data_stream:
if frame["frame_id"] > 5: # 只处理前5帧
break
print(frame)
4.3.2 生成器表达式
类似于列表推导式,但用圆括号。更简洁。
# 列表推导式:一次性生成所有数据
squares_list = [x*x for x in range(1000000)] # 占用大量内存
# 生成器表达式:按需生成
squares_gen = (x*x for x in range(1000000)) # 几乎不占内存
# 使用时
for val in squares_gen:
if val > 100:
break
print(val)
4.4 多线程与多进程:让仿真“跑起来”
仿真平台要同时处理很多事情:控制车辆、采集传感器数据、更新路况、渲染画面。如果所有事情都排队做,那仿真速度会慢得让人抓狂。这时候就需要并发编程。
Python里有两个主要工具:多线程 和 多进程。很多人搞不清它们的区别,我简单说一下。
| 特性 | 多线程 (threading) | 多进程 (multiprocessing) |
|---|---|---|
| 内存共享 | 共享同一进程内存 | 独立内存空间 |
| 适用场景 | I/O密集型(如网络请求、文件读写) | CPU密集型(如大量计算、图像处理) |
| GIL影响 | 受GIL限制,无法并行执行CPU任务 | 不受GIL影响,可充分利用多核 |
| 数据安全 | 需要加锁,容易出bug | 天然隔离,更安全 |
4.4.1 多线程:处理I/O密集型任务
在仿真中,多线程常用于处理传感器数据流、网络通信等。因为这些操作大部分时间都在等待(等待数据到达),而不是在计算。
import threading
import time
def sensor_reader(sensor_id):
"""模拟读取传感器数据"""
for i in range(5):
print(f"[线程-{sensor_id}] 读取第 {i} 帧数据")
time.sleep(0.5) # 模拟I/O等待
# 创建多个线程
threads = []
for sid in ["LIDAR", "RADAR", "CAMERA"]:
t = threading.Thread(target=sensor_reader, args=(sid,))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("所有传感器数据采集完成")
4.4.2 多进程:处理CPU密集型任务
如果你的仿真需要进行大量的物理计算、路径规划,那就得用多进程。每个进程有独立的Python解释器和内存空间,可以真正并行执行。
from multiprocessing import Process, Queue
import os
def heavy_computation(task_id, result_queue):
"""模拟一个耗时的计算任务"""
print(f"进程 {os.getpid()} 开始处理任务 {task_id}")
result = sum(i * i for i in range(10000000)) # 大量计算
result_queue.put((task_id, result))
print(f"进程 {os.getpid()} 完成任务 {task_id}")
if __name__ == "__main__":
queue = Queue()
processes = []
for i in range(4): # 启动4个进程
p = Process(target=heavy_computation, args=(i, queue))
processes.append(p)
p.start()
for p in processes:
p.join()
# 收集结果
while not queue.empty():
task_id, result = queue.get()
print(f"任务 {task_id} 的结果:{result}")
multiprocessing.shared_memory)或者Redis等中间件。别问我怎么知道的,都是泪。
4.4.3 在仿真中的实际应用
一个典型的仿真平台架构是这样的:
- 主进程:负责仿真逻辑、时间步进、状态更新
- 子进程1:负责物理引擎计算(碰撞检测、动力学)
- 子进程2:负责传感器仿真(激光雷达、摄像头)
- 子进程3:负责渲染和可视化
- 线程池:在每个进程内部,用多线程处理I/O操作(如读取配置文件、写入日志)
这样设计,既能利用多核CPU的计算能力,又能高效处理I/O,仿真速度能提升好几倍。
好了,这一章的内容就到这里。面向对象、装饰器、生成器、多线程与多进程,这些工具就像你工具箱里的扳手、螺丝刀、电钻。单独看都很简单,但组合起来,就能搭建出复杂的仿真系统。
下一章,我们会把这些知识用起来,开始搭建仿真平台的核心框架。到时候你会发现,原来这些基础概念,真的能派上大用场。