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 就是一个类,car1car2 就是对象。每个对象都有自己的 idpositionspeed,互不干扰。

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)
我的经验: 继承层次不要太深,一般3层以内就够了。我曾经见过一个7层继承的代码,改一个父类方法,下面所有子类都崩了,那叫一个酸爽。

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("所有传感器数据采集完成")
避坑指南: 我曾经在仿真中同时启动了几十个线程,结果因为线程切换开销太大,反而比单线程还慢。线程数不是越多越好,一般建议不超过CPU核心数的2倍。

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}")
我的建议: 多进程之间通信(IPC)是有开销的。如果数据量很大,可以考虑使用共享内存(multiprocessing.shared_memory)或者Redis等中间件。别问我怎么知道的,都是泪。

4.4.3 在仿真中的实际应用

一个典型的仿真平台架构是这样的:

  • 主进程:负责仿真逻辑、时间步进、状态更新
  • 子进程1:负责物理引擎计算(碰撞检测、动力学)
  • 子进程2:负责传感器仿真(激光雷达、摄像头)
  • 子进程3:负责渲染和可视化
  • 线程池:在每个进程内部,用多线程处理I/O操作(如读取配置文件、写入日志)

这样设计,既能利用多核CPU的计算能力,又能高效处理I/O,仿真速度能提升好几倍。


好了,这一章的内容就到这里。面向对象、装饰器、生成器、多线程与多进程,这些工具就像你工具箱里的扳手、螺丝刀、电钻。单独看都很简单,但组合起来,就能搭建出复杂的仿真系统。

下一章,我们会把这些知识用起来,开始搭建仿真平台的核心框架。到时候你会发现,原来这些基础概念,真的能派上大用场。