前面几篇把Python基础、NumPy、Matplotlib都过了一遍。今天聊一个跟ROS2直接相关的话题——Python节点和C++节点怎么在同一个系统里协作。

说个面试经历。

去年面一家做服务机器人的公司,前面聊得都挺好。面试官最后问了句:"你们那个项目里,有用Python写ROS2节点吗?和C++节点之间怎么通信的?"

我当时愣了一下,说:"用的话题通信……"

面试官追问:"那Python节点的性能瓶颈你怎么处理的?有没有遇到过序列化开销特别大的情况?"

这个问题我没准备好,回答得磕磕绊绊。后来才知道,混合编程在ROS2项目里太常见了——感知模块用C++保证性能,决策模块用Python提高开发效率,两者通过ROS2的通信机制协作。搞不清楚这套玩法,面试很容易吃亏。

ROS2里为什么需要混合编程

先说个背景。ROS2支持C++和Python两种语言的节点。这不是巧合,而是有意为之——机器人系统里不同模块的需求差异很大。

底层控制需要高频率、低延迟,比如电机控制跑1kHz,这种必须用C++。上层决策逻辑变化快、迭代频繁,比如任务调度和人机交互,用Python写效率高很多。中间的感知模块看情况——如果是传统视觉算法用C++,如果是深度学习推理可能用Python调PyTorch。

一个典型的服务机器人系统,可能是这样的架构:

C++节点负责底盘控制(100Hz)、激光SLAM(10Hz)、路径规划(5Hz)。Python节点负责物体识别(用YOLO,5Hz)、语音交互(异步事件触发)、任务管理(状态机逻辑)。两者之间通过ROS2的话题、服务、Action通信。

这种分工不是随便分的,而是根据每个模块的特性来选语言。面试的时候如果能说清楚这个决策逻辑,比单纯说"我用了Python"强太多。

用Python写一个ROS2节点

先看看Python节点长什么样。一个最简单的订阅者节点:

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan

class ObstacleDetector(Node):
    def __init__(self):
        super().__init__('obstacle_detector')
        self.sub = self.create_subscription(
            LaserScan, '/scan', self.scan_callback, 10)
        self.pub = self.create_publisher(
            Bool, '/obstacle_near', 10)
    
    def scan_callback(self, msg):
        min_dist = min(msg.ranges)
        result = Bool()
        result.data = min_dist < 0.5
        self.pub.publish(result)

def main():
    rclpy.init()
    node = ObstacleDetector()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

看起来和C++节点的结构差不多——初始化、创建订阅/发布、回调函数、spin循环。但Python版本写起来快很多,尤其是回调函数里的数据处理,用Python的内置函数几行就搞定。

有个细节要注意:Python里的min(msg.ranges)在数据量大时比较慢。如果LaserScan有720个点,每次回调都要遍历一遍。性能敏感的场景可以考虑用NumPy:np.min(msg.ranges),或者干脆把这个逻辑放到C++节点里。

Python和C++节点之间的通信方式

ROS2提供了几种进程间通信机制,混合编程时都会用到。

话题(Topic)是最常用的。C++节点发布传感器数据,Python节点订阅并处理。比如C++的激光驱动节点发布/scan,Python的避障节点订阅它。话题是单向的、异步的,适合数据流场景。

服务(Service)用于请求-响应模式。比如Python的任务管理节点需要知道当前电量,就调用C++的电池管理节点提供的/get_battery_status服务。调用是同步的(也有异步版本),适合"查一下状态"这种场景。

Action用于长时间运行的任务。比如Python节点让C++节点执行一段路径跟踪,中间可以反馈进度、也可以取消。导航用的就是Action模式。

实际项目中最容易踩的坑是消息序列化。ROS2默认用CDR序列化格式,C++和Python之间传递复杂消息(比如PointCloud2)时,序列化/反序列化的开销不小。一个720点的LaserScan还好,但一个10万点的点云,Python端接收时的反序列化可能要几十毫秒。

解决办法有几种。一种是降低数据频率——10万点的点云不需要100Hz传输,1Hz就够了。另一种是在C++端做预处理,把点云降采样之后再发给Python。还有一种是用共享内存——ROS2的rmw层支持共享内存传输,但配置起来比较麻烦。

一个实际的混合编程案例

说个我之前做的项目。一个移动抓取机器人,C++节点负责底盘运动和机械臂控制,Python节点负责物体检测和抓取策略。

整体架构是这样的:C++的底盘控制节点以50Hz发布里程计和激光数据。C++的机械臂控制节点提供关节控制服务。Python的物体检测节点订阅RGB-D相机图像,用YOLO做检测,发布物体位姿。Python的抓取规划节点订阅检测结果,计算抓取策略,然后通过Action调用C++的机械臂控制节点执行抓取。

这里有个有意思的问题。物体检测节点和抓取规划节点都在Python里,它们之间的数据传递其实不需要走ROS2的序列化——可以直接用Python的队列或者共享变量。但如果这两个节点未来可能要部署到不同机器上,还是用ROS2话题更规范。

我们当时选了ROS2话题。原因是:调试方便,用ros2 topic echo就能看到中间结果;而且后续确实把检测节点挪到了另一台带GPU的机器上。

面试中怎么聊混合编程

面试官问混合编程,核心想考察三点。

第一,你能不能根据模块特性选择合适的语言。不是"我都会用Python"或者"我只用C++",而是"底盘控制用C++因为需要1kHz实时性,任务管理用Python因为逻辑复杂且迭代频繁"。

第二,你清不清楚跨语言通信的开销和解决方案。序列化延迟、数据格式转换、频率匹配,这些都是实际工程中会遇到的问题。

第三,你有没有实际做过。如果你能画出一个混合编程的系统架构图,说清楚每个节点用什么语言、为什么这么选、中间怎么通信——面试官基本就满意了。

有个加分点:聊聊ROS2的component机制。C++节点可以编译成component,加载到同一个进程里,用intra-process通信避免序列化开销。但Python节点不支持component,所以Python和C++之间只能走进程间通信。这个限制在ROS2的iron和jazzy版本里有所改善,但本质上还是存在的。

混合编程的调试技巧

实际项目中Python和C++节点混合使用时,最容易遇到的问题就是数据类型不匹配。比如Python端发的是float64,C++端期望float32,结果数据全乱了。调试这类问题可以用ros2 topic echo查看消息的实际类型,也可以用rqt_graph看节点间的连接关系。另外,Python节点的启动速度比C++慢不少,在launch文件里要注意设置合适的启动顺序和延迟。

给正在准备面试的你

如果你之前只写过C++的ROS2节点,建议动手写几个Python节点试试。不用复杂,一个订阅者加一个发布者就够了。然后试着让Python节点和C++节点通信,跑通整个流程。

面试的时候,能说出"我用Python写了XX节点,和C++的YY节点通过话题通信,遇到了序列化延迟的问题,通过降采样解决了"——这种回答比背概念强十倍。

下篇聊Python在机器人测试中的应用——怎么用Python写自动化测试脚本,让你的机器人代码更可靠。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 上一篇:第73篇 Matplotlib可视化——调试SLAM/规划算法的必备技能 下一篇预告:第75篇 Python在机器人测试中的应用——自动化测试脚本编写

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐