第二章:Python与C++开发基础
2.1 为什么ROS2要支持两种语言?
很多刚接触ROS2的朋友会问我:为什么非要搞两套语言?用Python不香吗?或者全上C++不更统一?
说实话,这个问题我当年也纠结过。后来在项目里摔了几次跟头,才真正理解了这个设计哲学。
Python和C++在ROS2里不是竞争关系,而是互补关系。Python开发快、调试方便,适合做原型验证和工具脚本。C++性能强、实时性好,适合做核心算法和底层驱动。
我个人的习惯是:算法验证用Python,产品落地用C++。你想想看,一个激光雷达的驱动节点,每秒要处理几十万点云数据,用Python写?那CPU直接冒烟了。但反过来,一个调试用的可视化工具,用C++写?那开发周期够你喝一壶的。
核心结论:ROS2的rclcpp和rclpy共享同一个底层通信库(rcl),所以两种语言写的节点可以无缝通信。你完全可以在一个系统里混用Python和C++节点。
2.2 rclcpp与rclpy的底层设计
先看一张图,这是ROS2的软件栈分层结构:
看到这个分层结构了吗?rclcpp和rclpy都建立在同一个rcl层之上。rcl层封装了DDS的复杂性,提供了统一的C语言接口。所以不管上层用哪种语言,底层通信机制是完全一致的。
我记得刚开始接触ROS2时,以为rclpy就是rclcpp的简单Python包装。后来看了源码才发现,rclpy是用C扩展实现的,性能比纯Python包装好得多。但即便如此,Python的GIL(全局解释器锁)还是限制了它的并发能力。
避坑指南:我曾经在一个项目中,用Python写了图像处理节点,结果发现CPU占用率居高不下。后来换成C++实现,同样的算法,CPU占用率从85%降到了15%。所以,计算密集型的节点,请优先考虑C++。
2.3 编写第一个发布-订阅节点
好,理论说完了,咱们直接上手写代码。我会分别用Python和C++实现一个简单的发布-订阅节点对。
2.3.1 Python版本:发布者节点
# talker_py.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class TalkerNode(Node):
def __init__(self):
super().__init__('talker_py')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS2 from Python: {self.count}'
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = TalkerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2.3.2 Python版本:订阅者节点
# listener_py.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class ListenerNode(Node):
def __init__(self):
super().__init__('listener_py')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'I heard: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = ListenerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2.3.3 C++版本:发布者节点
// talker_cpp.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class TalkerNode : public rclcpp::Node
{
public:
TalkerNode() : Node("talker_cpp"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&TalkerNode::timer_callback, this));
}
private:
void timer_callback()
{
auto msg = std_msgs::msg::String();
msg.data = "Hello ROS2 from C++: " + std::to_string(count_++);
publisher_->publish(msg);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", msg.data.c_str());
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<TalkerNode>());
rclcpp::shutdown();
return 0;
}
2.3.4 C++版本:订阅者节点
// listener_cpp.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class ListenerNode : public rclcpp::Node
{
public:
ListenerNode() : Node("listener_cpp")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"chatter", 10,
std::bind(&ListenerNode::listener_callback, this, std::placeholders::_1));
}
private:
void listener_callback(const std_msgs::msg::String & msg)
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<ListenerNode>());
rclcpp::shutdown();
return 0;
}
2.4 两种语言的对比分析
写完了代码,我们来做个对比。我整理了一个表格,方便你直观地看到差异:
| 对比维度 | Python (rclpy) | C++ (rclcpp) |
|---|---|---|
| 代码行数 | 约30行 | 约50行 |
| 开发速度 | 快(无需编译) | 慢(需要CMake编译) |
| 运行性能 | 中等(受GIL限制) | 高(零拷贝、内联优化) |
| 内存管理 | 自动垃圾回收 | 智能指针(RAII) |
| 调试难度 | 低(动态类型、交互式) | 高(编译错误、段错误) |
| 适用场景 | 原型验证、工具脚本、配置管理 | 核心算法、实时控制、驱动层 |
注意:C++版本中我用了std::bind来绑定回调函数。在C++17及以上版本,你可以用lambda表达式替代,代码会更简洁。但为了兼容性,我建议在ROS2项目中统一使用std::bind。
2.5 编译与运行
Python节点不需要编译,直接运行即可:
# 终端1:运行Python发布者
python3 talker_py.py
# 终端2:运行Python订阅者
python3 listener_py.py
C++节点需要先编译:
# 在CMakeLists.txt中添加:
add_executable(talker_cpp talker_cpp.cpp)
target_link_libraries(talker_cpp rclcpp::rclcpp std_msgs::std_msgs)
add_executable(listener_cpp listener_cpp.cpp)
target_link_libraries(listener_cpp rclcpp::rclcpp std_msgs::std_msgs)
# 编译
colcon build --packages-select your_package
# 运行
ros2 run your_package talker_cpp
ros2 run your_package listener_cpp
你还可以混搭运行:比如用Python发布者 + C++订阅者,或者反过来。这就是ROS2的魅力所在——语言不再是障碍。
我的经验:刚开始学ROS2时,我建议你先用Python把逻辑跑通,然后再用C++重写性能瓶颈部分。这样既保证了开发效率,又保证了最终产品的性能。我在做自动驾驶感知模块时,就是先用Python验证算法,再用C++做工程化落地。
2.6 常见问题与避坑
- QoS设置不一致:发布者和订阅者的QoS配置必须匹配,否则收不到消息。我遇到过最坑的一次,就是忘了设置
durability策略,导致后启动的订阅者永远收不到之前发布的消息。 - 节点名称冲突:同一个ROS2网络中,节点名称必须唯一。我曾经在调试时开了两个同名节点,结果消息乱飞,排查了半天才发现是命名问题。
- Python的GIL问题:如果你的Python节点中有耗时操作,记得用
asyncio或multiprocessing来避免阻塞主线程。否则,整个节点的回调都会被卡住。
嗯,到这里,你应该已经掌握了ROS2中Python和C++的基本开发范式。两种语言各有千秋,关键是根据场景选对工具。下一节我们会深入节点的生命周期管理,到时候你会看到更多有意思的设计模式。
可可精品资料库