📺 推荐参考视频(实在搞不懂时去看):

  • ROS 实战部分:B站搜索 “鱼香ROS机器人”,该 UP 主的 ROS2 系列课程讲解细致,本文档是看这个视频的时候记录的
  • Python 类的基础语法:B站 林粒粒呀 的《三小时入门 Python》对"类"这个概念讲得通俗易懂,适合零基础快速理解面向对象编程

本文档作为上述视频的文字配套笔记,建议先看视频,本文档查阅具体语法细节作为辅助。


目录

  1. 什么是类?为什么需要类?
  2. 定义类的基本语法
  3. __init__ — 构造函数
  4. self — 对象自身
  5. 实例变量 vs 类变量
  6. 实例方法
  7. 封装
  8. 继承
  9. super().__init__() — 调用父类初始化
  10. 实例化
  11. 什么时候需要 __init__?什么时候不需要?
  12. 在 ROS2 中的固定套路
  13. 与导入模块的关系
  14. 术语速查表

1. 什么是类?为什么需要类?

类是一种"组织代码的方式"

把数据(属性)和操作数据的方法(函数)打包在一起,形成一个整体。这个整体就叫类。

为什么要用类?

如果不使用类,数据和行为是分散的:

# 不用类:数据归数据,函数归函数
name = "法外狂徒张三"
age = 18

def eat(person_name, person_age, food):
    print(f"我叫{person_name},今年{person_age}岁,吃{food}")

eat(name, age, "鱼香肉丝")

每次调用 eat 都要把 name 和 age 重新传一遍。数据和操作数据的行为是分离的。

使用类之后:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self, food):
        print(f"我叫{self.name},今年{self.age}岁,吃{food}")

p = Person("法外狂徒张三", 18)
p.eat("鱼香肉丝")  # 不需要再传 name 和 age,对象自己记得

数据(name、age)和行为(eat)绑定在一起。调用 eat 时对象知道自己的名字和年龄。


2. 定义类的基本语法

class 类名:
    # 类体(属性、方法)

示例:

class Download:
    def download(self, url, callback):
        ...
部分含义规则
classPython 关键字必须写
类名你自己取的名字使用大驼峰命名法(每个单词首字母大写),如 Download、PersonNode
:冒号表示类体开始,下面必须缩进

类名命名约定

class PersonNode      # ✅ 大驼峰:每个单词首字母大写
class person_node     # ❌ 不推荐:小写加下划线是变量/函数的命名方式
class personNode      # ❌ 不推荐:小驼峰不是 Python 类的规范

3. __init__ — 构造函数

是什么?

__init__ 是一个特殊方法(双下划线开头和结尾)。当你创建对象时,Python 会自动调用它。

作用?

在对象诞生时做初始化工作——把初始数据存到对象上。

语法

def __init__(self, 参数1, 参数2, ...):
    self.某个属性 = 参数1
    self.另一个属性 = 参数2

示例

class Person:
    def __init__(self, name, age):
        self.name = name    # 把传来的 name 存为这个对象的 name 属性
        self.age = age      # 把传来的 age 存为这个对象的 age 属性

当你写 p = Person("张三", 18) 时,Python 内部流程:

1. 创建一个空的 Person 对象
2. 自动调用 Person.__init__(这个空对象, "张三", 18)
3. __init__ 里:
   - self.name = "张三"    → 对象有了 name 属性
   - self.age = 18         → 对象有了 age 属性
4. 返回这个对象,赋给变量 p

什么时候写?什么时候不写?

情况需要 __init__?
创建对象时需要存储数据(后面方法中需要用到 self.xxx)✅ 需要
每次干活需要的数据都从参数直接传入,不需要提前存储❌ 不需要

不写 __init__ 的例子:

class Download:
    def download(self, url, callback):
        # url 和 callback 每次调用时直接从参数传入,不需要存
        ...

4. self — 对象自身

self 是什么?

self 代表调用这个方法的那个对象本身。

为什么每个方法都要写 self?

Python 规定:实例方法的第一个参数必须是 self(虽然你可以给它取别的名字,但全世界都用 self)。

调用时不需要手动传 self

class Dog:
    def bark(self):
        print(f"{self.name} 在叫")   # self 就是调用时的那个对象

dog1 = Dog()
dog1.bark()   # 你写了 dog1.bark(),Python 自动把 dog1 传给 self

self 的用途:访问对象的属性和方法

class Person:
    def __init__(self, name):
        self.name = name        # 把 name 存为 self 的属性

    def greet(self):
        print(f"我是{self.name}")  # 通过 self 访问这个对象的 name 属性

5. 实例变量 vs 类变量

实例变量

属于每个对象自己,每个对象的值可以不同。在 __init__ 里用 self.xxx 定义。

class Dog:
    def __init__(self, name):
        self.name = name    # 实例变量:每只狗的名字不同

dog1 = Dog("旺财")
dog2 = Dog("来福")
print(dog1.name)  # 旺财
print(dog2.name)  # 来福

类变量

属于整个类,所有对象共享同一份数据。在类体里、方法外面定义。

class Dog:
    species = "犬科"       # 类变量:所有狗都是犬科

    def __init__(self, name):
        self.name = name   # 实例变量

dog1 = Dog("旺财")
dog2 = Dog("来福")
print(dog1.species)  # 犬科
print(dog2.species)  # 犬科
实例变量类变量
属于谁每个对象自己整个类,所有对象共享
定义位置__init__ 里,用 self.xxx类体里、方法外
访问方式self.xxx类名.xxx 或 self.xxx
每个对象值相同吗?❌ 可以不同✅ 所有对象一样

6. 实例方法

定义在类里面的函数叫方法。第一个参数必须是 self。

class Person:
    def eat(self, food_name):           # self + 其他参数
        print(f"吃{food_name}")

    def sleep(self, hours):             # 可以有多个自定义方法
        print(f"睡{hours}小时")

调用方式:

p = Person()
p.eat("鱼香肉丝")    # 对象.方法名(参数),不需要传 self

7. 封装

封装的意思是:把数据和操作数据的方法捆绑在一起,隐藏内部细节,对外只暴露简单的接口。

class Download:
    def download(self, url, callback):
        # 内部:怎么发 HTTP 请求、怎么处理编码...
        response = requests.get(url)
        response.encoding = 'utf-8'
        callback(url, response.text)

    def start_download(self, url, callback):
        # 内部:怎么开线程...
        thread = threading.Thread(target=self.download, args=(url, callback))
        thread.start()

外部使用时:

d = Download()
d.start_download('http://...', my_callback)

使用者只需要知道"调用 start_download 并传网址和回调函数",不需要知道内部是怎么开线程、怎么发 HTTP 请求的。


8. 继承

语法

class 子类名(父类名):
    ...

含义

子类自动获得父类的所有属性和方法,不用重新写一遍。子类还可以添加自己的新功能。

代码示例

# 父类
class PersonNode(Node):
    def __init__(self, node_name, name, age):
        super().__init__(node_name)
        self.name = name
        self.age = age

    def eat(self, food_name):
        self.get_logger().info(f'我叫{self.name},吃{food_name}')

# 子类:继承 PersonNode
class WriterNode(PersonNode):
    def __init__(self, name, age, book):
        super().__init__(name, age)   # 调用父类的 __init__
        self.book = book              # 新增自己的属性

继承链和能力叠加

Node              ← ROS2 提供:get_logger()、create_publisher() 等
  └── PersonNode   ← 加了:name、age、eat()
        └── WriterNode  ← 加了:book

WriterNode 的对象同时拥有这三层的全部能力。

为什么用继承?

  • 不用重复造轮子:WriterNode 不需要重写 eat(),直接继承就能用
  • 层次清晰:每一层只管自己的新增部分
  • 容易扩展:需要新类型时再继承一层即可

9. super().__init__() — 调用父类初始化

含义

super() 返回当前类的父类。super().__init__(...) 就是调用父类的 __init__,让父类先完成它的初始化工作。

为什么要调用它?

父类可能有自己的初始化逻辑(比如把节点名注册到 ROS2 系统)。如果不调用,父类的初始化就没执行,后续功能可能失效。

在 ROS2 节点中必须写

class PersonNode(Node):
    def __init__(self, node_name, name, age):
        super().__init__(node_name)    # 必须!把节点名登记到 ROS2 系统
        ...

如果删掉这行,ROS2 不知道这个节点存在,get_logger()、发布、订阅统统失效。

多层继承中的 super()

Node.__init__          ← 最底层:向 ROS2 登记
  ↑ 被调用
PersonNode.__init__    ← 中间层:存 name、age
  ↑ 被调用
WriterNode.__init__    ← 最上层:存 book

每一层的 super().__init__() 往上调用,最终整条链都执行。


10. 实例化

语法

对象变量 = 类名(参数1, 参数2, ...)

含义

用"类"这个模板,造出一个具体的"对象"。

示例

# 不需要 __init__ 的类
d = Download()                                    # 空括号

# 需要 __init__ 的类
node = PersonNode('person_node', '法外狂徒张三', 18)  # 传参数

实例化时 Python 做了什么?

node = PersonNode('person_node', '法外狂徒张三', 18)

内部流程:

1. 分配内存 → 创建一个空的 PersonNode 对象
2. 自动调用 PersonNode.__init__(空对象, 'person_node', '法外狂徒张三', 18)
3. __init__ 内部执行:
   → super().__init__('person_node')    向 ROS2 登记
   → self.age = 18                      存年龄
   → self.name = '法外狂徒张三'          存名字
4. 返回创建好的对象,赋给变量 node

11. 什么时候需要 __init__?什么时候不需要?

需要 __init__

对象有自己的"记忆"——方法里要用到 self.xxx:

class PersonNode(Node):
    def __init__(self, node_name, name, age):
        super().__init__(node_name)
        self.age = age          # 存起来
        self.name = name        # 存起来

    def eat(self, food_name):
        # 这里用到 self.name 和 self.age,所以必须先在 __init__ 里存
        self.get_logger().info(f'我叫{self.name},今年{self.age}岁,吃{food_name}')

不需要 __init__

每次方法调用需要的所有东西都从参数直接传入,不需要存:

class Download:
    def download(self, url, callback):
        # url 和 callback 从参数直接来,不需要 self.url
        ...

    def start_download(self, url, callback):
        # 同理
        ...

12. 在 ROS2 中的固定套路

以后你写任何 ROS2 Python 节点,都遵循这个骨架:

import rclpy
from rclpy.node import Node

class 你的节点名(Node):                 # ① 继承 Node
    def __init__(self, ...):            # ② 构造函数
        super().__init__('节点名')       # ③ 必须!向 ROS2 登记
        self.xxx = ...                  # ④ 你自己的属性
        # ⑤ 在这里创建发布者、订阅者、定时器...

    def 你的方法(self, ...):            # ⑥ 自定义方法
        ...

def main(args=None):
    rclpy.init(args=args)
    node = 你的节点名(...)              # ⑦ 实例化
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

13. 与导入模块的关系

从系统库导入

from rclpy.node import Node

翻译:去 rclpy 库的 node.py 文件里,把 Node 类拿出来给我用。

从你自己写的文件导入

from demo_python_pkg.person_node import PersonNode

翻译:去 demo_python_pkg 包的 person_node.py 文件里,把 PersonNode 类拿出来。

这样 WriterNode 就可以继承 PersonNode,不用把代码重写一遍。


14. 术语速查表

术语含义示例
类(Class)一个模板,定义对象的属性和行为class PersonNode:
对象(Object)用类造出来的具体实例node = PersonNode(...)
实例化用类创建对象的过程d = Download()
__init__构造函数,创建对象时自动调用def __init__(self, ...):
self代表对象自身,方法第一个参数self.name = name
实例变量属于每个对象自己的数据self.age
类变量属于整个类、所有对象共享的数据species = "犬科"
实例方法定义在类里的函数,第一个参数是 selfdef eat(self, food):
封装隐藏内部细节,对外暴露简单接口start_download 内部开线程,外部不用管
继承子类自动获得父类的所有能力class WriterNode(PersonNode):
super()指代父类super().__init__(...)
父类 / 子类被继承的类叫父类,继承的类叫子类Node 是父类,PersonNode 是子类

📝 核心口诀:类是把数据和行为打包的模板;继承让你不用重复造轮子;super().__init__() 是 ROS2 节点的必写项;self 永远代表"那个对象自己"。实在搞不懂时,回到开头的视频链接,跟着林粒粒呀和鱼香ROS机器人再走一遍。

更多推荐