01 · Python 基础速成

这一章要解决什么问题:假设你只写过 print("hello"),怎么在最短时间内看懂机器人代码?

本章不讲 Python 的全部,只讲读机器人代码时真正用得到的那部分

配套代码:code/ch01_python_basics.py


本章学习目标

读完本章,你将能够:

  1. 读懂机器人代码中的基本语法:变量、列表、字典、循环、函数、类——这些是 MuJoCo/ikpy/dm_control 代码的"词汇表"。
  2. 理解类与对象的关系:能解释 model = mujoco.MjModel.from_xml_path(...) 这行代码在做什么,self 是什么意思。
  3. 掌握角度与弧度的转换:知道为什么机器人代码内部一律用弧度,以及如何在显示时转成角度。
  4. 使用 f-string 格式化输出:能打印出类似 EE=[0.300, 0.040, 0.180] 这样的调试信息。
  5. 独立完成本章的 4 个练习:包括关节限位检查、角度转换、定义类、轨迹采样。

📌 前置知识:本章需要你会写 print()for 循环、if 判断。如果你连这些都不会,建议先花 1 小时看任意 Python 入门教程的前 3 章。


1.1 为什么机器人代码"看起来不一样"

随便打开一段 MuJoCo 代码,你会看到这样的东西:

model = mujoco.MjModel.from_xml_path("arm.xml")
data = mujoco.MjData(model)
data.ctrl[:6] = target_q
for _ in range(100):
    mujoco.mj_step(model, data)

逐行解读(现在看不懂没关系,本章会把每个概念拆开讲):

代码 在做什么 涉及的概念
1 model = mujoco.MjModel.from_xml_path("arm.xml") 从 XML 文件加载机器人模型,创建一个 MjModel 对象 类、对象、类方法
2 data = mujoco.MjData(model) 创建一个 MjData 对象,存储仿真的动态状态(关节角、速度等) 类、构造方法
3 data.ctrl[:6] = target_q 把目标关节角赋值给控件数组的前 6 个元素 属性、列表切片
4 for _ in range(100): 循环 100 次(_ 表示"这个变量我不用") for 循环
5 mujoco.mj_step(model, data) 推进仿真一个时间步 模块级函数

看起来陌生的其实只有三件事:对象(类)赋值(数组)循环。这三件事本章都会讲到。

Python 在机器人领域流行,不是因为它快(它很慢),而是因为它读起来像伪代码,能让你把精力放在算法而不是语法上。


1.2 运行 Python 代码的三种方式

# 方式 1:运行脚本文件(最常用,本书所有示例都这样跑)
python ch01_python_basics.py

# 方式 2:一行命令
python -c "print(1 + 1)"

# 方式 3:交互式(探索 API 时极其有用,强烈推荐)
python
>>> import numpy as np
>>> np.array([1, 2, 3])

💡 强烈建议你养成用交互模式的习惯。遇到不熟悉的 API(比如 mujoco.MjData 有哪些属性),直接 python 进去 dir() 一下,比查文档快得多。


1.3 变量与类型:不需要声明

Python 是动态类型语言,变量不需要声明类型,直接赋值:

angle = 0.5            # 浮点数 float
num_joints = 6         # 整数 int
robot_name = "CX4"     # 字符串 str
is_moving = True       # 布尔 bool(注意首字母大写)

type() 查看类型:

print(type(angle))       # <class 'float'>
print(type(num_joints))  # <class 'int'>

类型转换——这在读传感器数据时天天用:

angle_deg = 90
angle_rad = angle_deg * 3.14159 / 180    # 角度 → 弧度
print(f"{angle_deg}° = {angle_rad:.4f} rad")

类型转换的完整图景

int ──┐
      ├──> float ──> str
float ┘
str ──> int / float(用 int() / float(),但字符串必须是合法数字)
# 常见转换
x = int(3.7)           # 3(截断小数,不是四舍五入!)
y = float("3.14")      # 3.14(字符串转浮点数)
s = str(3.14)          # "3.14"(数字转字符串)
b = bool(0)            # False(0 是 False,非 0 是 True)

⚠️ 常见错误int(3.7) 得到 3 而不是 4——Python 的 int()截断不是四舍五入。如果需要四舍五入,用 round(3.7)

⚠️ 机器人领域第一大坑:角度与弧度
Python 的 math.sin()弧度;而机器人示教器、关节限位通常用角度
本书约定:代码内部一律用弧度,只在显示给用户时才转成角度。

为什么用弧度? 因为微积分和物理公式(如角速度、力矩)都是基于弧度的。用弧度可以避免在公式里反复乘除 π/180,减少出错机会。

转换速查

  • 角度 → 弧度:rad = deg * π / 180,或 math.radians(deg)
  • 弧度 → 角度:deg = rad * 180 / π,或 math.degrees(rad)

1.4 容器:列表、元组、字典

1.4.1 列表 list —— 最常用的容器

joint_angles = [0.0, 0.3, -0.5, 0.0, 0.2, 0.0]   # 6 个关节角

列表的核心操作:

joint_angles[0]        # 取第 1 个 → 0.0(下标从 0 开始!)
joint_angles[-1]       # 取最后 1 个 → 0.0
joint_angles[1:3]      # 切片:取下标 1 和 2 → [0.3, -0.5](不含末尾 3)
len(joint_angles)      # 长度 → 6
joint_angles[0] = 0.1  # 修改(列表可变)
joint_angles.append(0.5)  # 追加

切片可视化(以 joint_angles = [0.0, 0.3, -0.5, 0.0, 0.2, 0.0] 为例):

下标:   0    1     2    3    4    5
       ┌───┬────┬─────┬───┬────┬───┐
值:    │0.0│0.3 │-0.5 │0.0│0.2 │0.0│
       └───┴────┴─────┴───┴────┴───┘
负下标: -6   -5    -4   -3   -2   -1

joint_angles[1:3]  →  取下标 1,2  →  [0.3, -0.5]
                       ↑           ↑
                     start=1     stop=3(不含)

joint_angles[:3]   →  从开头到下标 2  →  [0.0, 0.3, -0.5]
joint_angles[3:]   →  从下标 3 到末尾  →  [0.0, 0.2, 0.0]
joint_angles[::2]  →  每隔 2 个取一个  →  [0.0, -0.5, 0.2]

⚠️ 切片 a[start:stop] 不含 stop。这是 Python 最容易记错的点之一。

记忆方法a[1:3] 可以理解为"从第 1 个开始,取 3-1=2 个元素"。这样就不会记错了。

1.4.2 元组 tuple —— 不可变的列表

position = (0.3, 0.1, 0.5)     # 用圆括号,创建后不能改
x, y, z = position             # 解包(unpack),一行拿到三个值

元组常用于不应该被修改的数据,比如坐标。

1.4.3 字典 dict —— 键值对

joint_limits = {
    "J1": (-180, 180),
    "J2": (-155, 67),
    "J3": (-63, 193),
}
print(joint_limits["J2"])       # → (-155, 67)

字典在机器人里最常见的用途是按名字组织参数

robot_state = {
    "qpos": [0.0, 0.3, -0.5],
    "qvel": [0.0, 0.0, 0.0],
    "timestamp": 1.25,
}

💡 列表 vs 字典:列表用数字下标a[0]),字典用名字a["J2"])。
机器人里 data.qpos[3](第 4 个关节)和 physics.named.data.qpos["j4"](叫 j4 的关节)就是这两种风格的典型代表。


1.5 字符串与 f-string

f-string 是格式化输出的最好方式(Python 3.6+):

name = "J2"
angle = 45.123456
print(f"关节 {name} 当前角度:{angle:.2f}°")     # 保留 2 位小数
print(f"关节 {name} 当前角度:{angle:8.3f}°")    # 总宽 8 字符,3 位小数

输出:

关节 J2 当前角度:45.12°
关节 J2 当前角度:  45.123°

:.2f 中的 .2 表示保留 2 位小数,8.3f 中的 8 表示最小宽度(用于对齐打印)。


1.6 控制流

# if / elif / else
if angle > 3.0:
    print("超出限位!")
elif angle < -3.0:
    print("低于限位!")
else:
    print("正常")

# for 循环
for i in range(6):                    # 0,1,2,3,4,5
    print(f"关节 {i}")

for name, limit in joint_limits.items():    # 遍历字典
    print(f"{name}: {limit}")

# while 循环(仿真主循环常用)
step = 0
while step < 100:
    step += 1

仿真主循环通常用 while viewer.is_running() 这种形式——条件为假时自动退出


1.7 函数

def degrees_to_radians(deg):
    """把角度转成弧度。"""          # 文档字符串(docstring)
    return deg * 3.141592653589793 / 180.0

r = degrees_to_radians(90)
print(r)          # 1.5707963267948966

函数解剖图

┌─────────────────────────────────────────────────────┐
│  def  degrees_to_radians(deg):                       │
│   ↑      ↑                  ↑                         │
│ 关键字  函数名            参数(输入)                 │
│                                                        │
│      """把角度转成弧度。"""    ← 文档字符串(说明)    │
│                                                        │
│      return deg * π / 180     ← 函数体(做什么)      │
│             ↑                    ← 返回值(输出)      │
└─────────────────────────────────────────────────────┘

调用:degrees_to_radians(90)
       │
       ▼
  deg = 90(参数传入)
       │
       ▼
  计算 90 * π / 180 = 1.5708
       │
       ▼
  返回 1.5708 → 赋值给 r

💡 函数的输入和输出

  • 输入:参数(deg),可以有 0 个或多个
  • 输出return 后面的值,如果没有 return 则默认返回 None
  • 内部做了什么:函数体里的代码
  • 为什么这样写:把重复的逻辑封装起来,调用时只需要一行

多个返回值(Python 其实返回了一个元组):

def get_pose():
    position = [0.3, 0.1, 0.5]
    orientation = [1.0, 0.0, 0.0, 0.0]   # 四元数
    return position, orientation

pos, quat = get_pose()        # 解包接收

默认参数

def move_to(target, speed=0.5, timeout=10.0):
    """移动到目标位置;speed 和 timeout 有默认值,可不传。"""
    print(f"以 {speed} m/s 移动到 {target},超时 {timeout}s")

move_to([0.3, 0.1, 0.5])              # 用默认速度
move_to([0.3, 0.1, 0.5], speed=0.2)   # 指定速度

💡 机器人代码里到处是 def solve_ik(target, initial_qpos=None, max_iter=300, tol=1e-4) 这样的函数——默认参数让你只关心重要的那几个参数


1.8 类与对象(最重要的一节)

如果你只打算认真学本章一节,就学这一节。

因为 ikpy、MuJoCo、dm_control 这三个框架,全都是用类组织的

1.8.1 什么是类

类(class)= 图纸;对象(object/instance)= 按图纸造出来的东西

类与对象的关系图

图纸(类)                    实物(对象)
┌──────────────┐
│  RobotArm    │    造一个    ┌──────────────────┐
│  ──────────  │ ──────────▶  │  arm = RobotArm( │
│  name        │               │    "CX4", 6)     │
│  num_joints  │               ├──────────────────┤
│  joint_angles│               │  arm.name        │
│  ──────────  │               │    = "CX4"       │
│  set_joint() │               │  arm.num_joints  │
│  get_joint() │               │    = 6            │
└──────────────┘               │  arm.joint_angles│
                                │    = [0,0,0,0,0,0]│
                                └──────────────────┘

可以用同一张图纸造多个对象:
arm1 = RobotArm("CX4-A", 6)
arm2 = RobotArm("CX4-B", 6)
# arm1 和 arm2 是独立的,改 arm1 的关节角不影响 arm2
class RobotArm:
    """一个简化到极致的机械臂类。"""

    def __init__(self, name, num_joints):
        """构造方法:创建对象时自动调用。self 指"这个对象自己"。"""
        self.name = name                          # 属性
        self.num_joints = num_joints
        self.joint_angles = [0.0] * num_joints    # 6 个 0.0

    def set_joint(self, index, angle):
        """方法:设置某个关节的角度。"""
        self.joint_angles[index] = angle

    def get_joint(self, index):
        return self.joint_angles[index]

    def __repr__(self):
        """定义 print(对象) 时显示什么。"""
        return f"<RobotArm {self.name}, joints={self.num_joints}>"

使用它:

arm = RobotArm("CX4-A601C", 6)     # 创建对象(不用传 self!)
print(arm)                          # <RobotArm CX4-A601C, joints=6>
arm.set_joint(2, 0.75)
print(arm.get_joint(2))             # 0.75
print(arm.joint_angles)             # 直接访问属性

1.8.2 self 是什么

self 就是对象自己。当你写 arm.set_joint(2, 0.75) 时,Python 实际调用的是:

RobotArm.set_joint(arm, 2, 0.75)    # arm 被作为第一个参数传进去

所以 self.joint_angles[index] = angle 实际是 arm.joint_angles[index] = angle

self 的生活类比

想象你在一个教室里,老师说"举起你的右手"。每个人都会举起自己的右手,而不是同一个人的右手。

  • self 就相当于"你的"——每个对象都有自己的属性。
  • self.joint_angles 就是"这个对象自己的关节角"。
  • arm1.set_joint(0, 0.5) 时,self 指的是 arm1
  • arm2.set_joint(0, 0.3) 时,self 指的是 arm2

__init__ 构造方法详解

def __init__(self, name, num_joints):
    self.name = name              # 把参数 name 存到对象的 name 属性里
    self.num_joints = num_joints  # 把参数 num_joints 存起来
    self.joint_angles = [0.0] * num_joints   # 初始化关节角为全 0

__init__ 在你创建对象时自动调用,不需要手动写 arm.__init__(...)。它的作用是"给新对象设置初始状态"。

记住:定义方法时第一个参数必须写 self,但调用时不需要传它

1.8.3 对应到真实框架

现在你就能读懂这些了:

model = mujoco.MjModel.from_xml_path("arm.xml")   # 创建 MjModel 对象
data  = mujoco.MjData(model)                       # 创建 MjData 对象
data.ctrl[:6] = target_q                           # 访问 data 的属性
mujoco.mj_step(model, data)                        # 模块级函数
  • MjModel / MjData
  • model / data对象
  • data.ctrl属性
  • mujoco.mj_step(...)模块级函数(不属于任何类)

1.8.4 类方法 vs 静态方法

类里有两种"不需要 self"的特殊方法,机器人代码里经常见到:

静态方法 @staticmethod:不需要 self,也不依赖对象的状态。它本质上就是一个放在类命名空间里的普通函数。

class RobotArm:
    def __init__(self, name, num_joints):
        self.name = name
        self.num_joints = num_joints
        self.joint_angles = [0.0] * num_joints

    def set_joint(self, index, angle):
        """设置某个关节的角度。"""
        if not (0 <= index < self.num_joints):
            raise IndexError(f"关节下标 {index} 越界(共 {self.num_joints} 个)")
        self.joint_angles[index] = angle

    @staticmethod
    def deg2rad(deg):
        """静态方法:把角度转成弧度。不需要 self,不依赖对象状态。"""
        return deg * 3.141592653589793 / 180.0

调用静态方法不需要先创建对象,直接用类名调用:

# 不需要 arm = RobotArm(...),直接用类名
print(RobotArm.deg2rad(180))      # 3.14159...

# 也可以通过对象调用(效果一样)
arm = RobotArm("CX4", 6)
print(arm.deg2rad(90))             # 1.5708...

什么时候用静态方法? 当一个工具函数逻辑上属于这个类,但又不需要访问对象的属性时。比如"角度转弧度"这个操作跟机械臂相关,但不需要知道具体是哪台机械臂。

类方法 @classmethod:第一个参数是 cls(类本身),不是 self。常用于"替代构造函数"——根据不同的输入创建对象。

class RobotArm:
    def __init__(self, name, num_joints):
        self.name = name
        self.num_joints = num_joints

    @classmethod
    def from_config(cls, config_dict):
        """从配置字典创建对象(类方法的典型用途:替代构造函数)。"""
        return cls(config_dict["name"], config_dict["num_joints"])

# 用类方法创建对象
arm = RobotArm.from_config({"name": "CX4", "num_joints": 6})

mujoco.MjModel.from_xml_path(...) 就是一个类方法——不需要先有 model 对象,直接调用它来"造"一个。


1.9 模块与包(import 的几种写法)

import numpy                      # 导入整个模块,用 numpy.array()
import numpy as np                # 起别名(业界标准,照抄即可)
from math import sin, cos         # 只导入需要的函数,直接用 sin()
from ikpy.chain import Chain      # 从子模块导入某个类
from pathlib import Path          # 处理文件路径

📌 约定import numpy as npimport matplotlib.pyplot as plt 是全世界的默认写法,请照抄,别自创别名。

路径处理(用 pathlib,别拼字符串):

from pathlib import Path
# 基于当前文件位置推导项目根目录,不依赖运行时的工作目录
PROJECT_ROOT = Path(__file__).resolve().parent.parent
model_path = PROJECT_ROOT / "models" / "cx4_a601c_simulation.xml"
print(model_path.exists())     # 检查文件是否存在

1.10 列表推导式(一行循环)

Python 的特色语法,机器人代码里到处都是:

# 传统写法
angles_deg = []
for a in [0, 30, 45, 90]:
    angles_deg.append(a * 3.14159 / 180)

# 列表推导式(等价,更简洁)
angles_rad = [a * 3.14159 / 180 for a in [0, 30, 45, 90]]

带条件:

# 只保留在限位内的角度
valid = [a for a in angles if -3.14 < a < 3.14]

1.11 异常处理(读别人的代码时很有用)

try:
    value = joint_limits["J9"]      # 不存在的键
except KeyError as e:
    print(f"没有这个关节:{e}")

机器人代码常见用法——优雅降级

try:
    import imageio                  # 可选依赖
    CAN_RECORD = True
except ImportError:
    print("imageio 不可用,录制功能已禁用")
    CAN_RECORD = False

主动抛出异常 raise:不只是捕获别人的异常,你自己的代码也可以在检测到错误时主动抛出。这在机器人代码里非常重要——比如关节下标越界、目标点不可达,都应该主动报错而不是静默返回错误结果。

class RobotArm:
    def __init__(self, num_joints):
        self.num_joints = num_joints
        self.joint_angles = [0.0] * num_joints

    def set_joint(self, index, angle):
        """设置某个关节的角度。下标越界时主动抛出 IndexError。"""
        if not (0 <= index < self.num_joints):
            raise IndexError(f"关节下标 {index} 越界(共 {self.num_joints} 个)")
        self.joint_angles[index] = angle

arm = RobotArm(6)
try:
    arm.set_joint(99, 0.0)       # 下标 99 越界了
except IndexError as e:
    print(f"捕获到异常: {e}")     # 捕获到异常: 关节下标 99 越界(共 6 个)

常用的内置异常类型

异常类型 触发场景 机器人代码中的例子
IndexError 列表/数组下标越界 访问第 10 个关节,但只有 6 个
KeyError 字典里没有这个键 joint_limits["J9"],但只有 J1~J6
ValueError 参数值不合法 传入负的连杆长度、超出物理范围的角度
TypeError 参数类型不对 给期望数字的函数传了字符串
RuntimeError 运行时逻辑错误 IK 求解不收敛、仿真发散

💡 什么时候该 raise,什么时候该 return None? 原则:如果调用者没有正确处理这个错误,程序应该崩溃而不是继续错下去。关节下标越界这种编程错误,应该 raise;而"目标点暂时不可达"这种运行时可恢复的情况,可以返回错误码或 None 让调用者决定。


1.12 这些语法在机器人代码里的位置

语法 在本书什么地方出现
列表 关节角 q = [0.0, 0.2, 0.6, 0.0, 0.3, 0.0]
字典 关节限位表、observation 字典
f-string 打印关节状态 print(f"EE=[{x:.3f},{y:.3f}]")
for + range 仿真主循环 for _ in range(max_steps)
while while viewer.is_running()
ChainMjModelMjDataTask
模块导入 import mujocofrom dm_control import mjcf
列表推导式 批量处理轨迹点
try/except 可选依赖(imageio)的优雅降级

1.13 动手练

  1. 关节限位检查器:写一个函数 check_limits(angles_deg, limits),接收角度列表和限位字典,返回超限的关节名。limits = {"J1": (-180,180), "J2": (-155,67)}

  2. 角度转换:把 [0, 30, 45, 60, 90] 转成弧度并打印,保留 4 位小数。

  3. 定义一个 JointState:属性 nameanglevelocity;方法 to_dict() 返回字典。创建 3 个关节对象并打印。

  4. (挑战)轨迹采样:函数 sample_line(p0, p1, n) 返回从 p0p1n 个等间距点(每个点是 3 元素列表)。这是第 21 章轨迹规划的基础。

参考答案与解析

练习 1:关节限位检查器

def check_limits(angles_deg, limits):
    """检查哪些关节超出了限位。
    输入:angles_deg = [角度列表], limits = {"关节名": (下限, 上限)}
    输出:超限关节名的列表
    """
    over = []
    for i, (name, (lo, hi)) in enumerate(limits.items()):
        angle = angles_deg[i]
        if angle < lo or angle > hi:
            over.append(name)
    return over

# 测试
limits = {"J1": (-180, 180), "J2": (-155, 67), "J3": (-63, 193)}
print(check_limits([0, 80, 0], limits))    # ['J2'] —— 80° 超过了 J2 的上限 67°

解析

  • enumerate(limits.items()) 同时拿到下标 i 和键值对 (name, (lo, hi))
  • 用下标 iangles_deg 取对应关节的角度。
  • 判断是否超出 [lo, hi] 范围,超出就加入结果列表。

练习 2:角度转换

import math
angles_deg = [0, 30, 45, 60, 90]
angles_rad = [math.radians(a) for a in angles_deg]   # 列表推导式
for deg, rad in zip(angles_deg, angles_rad):
    print(f"{deg:>3}° = {rad:.4f} rad")

输出:

  0° = 0.0000 rad
 30° = 0.5236 rad
 45° = 0.7854 rad
 60° = 1.0472 rad
 90° = 1.5708 rad

解析math.radians(a) 等价于 a * π / 180,比手写更清晰。zip() 把两个列表配对,方便同时遍历。

练习 3:JointState 类

class JointState:
    """记录一个关节的状态:名字、角度、角速度。"""
    def __init__(self, name, angle, velocity):
        self.name = name
        self.angle = angle
        self.velocity = velocity

    def to_dict(self):
        """把关节状态转成字典(方便序列化/保存)。"""
        return {
            "name": self.name,
            "angle": self.angle,
            "velocity": self.velocity,
        }

    def __repr__(self):
        return f"<Joint {self.name}: angle={self.angle:.3f}, vel={self.velocity:.3f}>"

# 创建 3 个关节
joints = [
    JointState("J1", 0.0, 0.0),
    JointState("J2", 0.5236, 0.1),
    JointState("J3", -0.7854, -0.05),
]
for j in joints:
    print(j)
    print(j.to_dict())

解析__repr__ 方法定义了 print(对象) 时的显示格式,让调试输出更友好。to_dict() 是实际项目中常用的模式——把对象转成字典,方便存 JSON 或传给其他系统。

练习 4:轨迹采样(挑战题)

def sample_line(p0, p1, n):
    """从 p0 到 p1 生成 n 个等间距点。
    输入:p0, p1 = [x, y, z], n = 点数
    输出:n 个点的列表,每个点是 [x, y, z]
    """
    points = []
    for i in range(n):
        t = i / (n - 1) if n > 1 else 0.0   # t 从 0 到 1
        x = p0[0] + (p1[0] - p0[0]) * t
        y = p0[1] + (p1[1] - p0[1]) * t
        z = p0[2] + (p1[2] - p0[2]) * t
        points.append([x, y, z])
    return points

# 测试:从原点到 (0.3, 0.1, 0.2),5 个点
pts = sample_line([0, 0, 0], [0.3, 0.1, 0.2], 5)
for p in pts:
    print(f"[{p[0]:.3f}, {p[1]:.3f}, {p[2]:.3f}]")

输出:

[0.000, 0.000, 0.000]
[0.075, 0.025, 0.050]
[0.150, 0.050, 0.100]
[0.225, 0.075, 0.150]
[0.300, 0.100, 0.200]

解析

  • t = i / (n - 1) 让第一个点 t=0(=p0),最后一个点 t=1(=p1)。
  • n=1n-1=0 会除零,所以加了 if n > 1 的判断。
  • 这是线性插值(Lerp),第 21 章会在此基础上做更复杂的轨迹规划(如五次多项式、S 曲线)。

完整可运行代码见 code/ch01_python_basics.py 末尾。


1.14 Python 编码规范与机器人代码风格

好的编码规范能让代码更易读、更易维护。机器人代码尤其需要清晰,因为出 bug 时可能导致机械臂碰撞。

命名规范

类型 规范 示例
变量 小写+下划线(snake_case) joint_angles, target_pos, ee_id
常量 全大写+下划线 IK_DAMPING, PICK_POS, MAX_ITER
函数 小写+下划线 forward_kinematics, solve_ik, clip_limits
大驼峰(PascalCase) RobotArm, IKSolver, PickAndPlaceTask
模块 小写+下划线 ch02_math_3d, pick_and_place
# ✅ 好的命名
q_current = data.qpos[:6].copy()
error = target_pos - ee_pos
dq = solve_ik(jacobian, error)

# ❌ 不好的命名
a = data.qpos[:6]        # a 是什么?
b = t - e                # t 和 e 是什么?
c = f(d, b)              # 完全看不懂

机器人代码的常见缩写

缩写 全称 含义
q joint positions 关节角
dq / qvel joint velocities 关节速度
ee end-effector 末端执行器
pos position 位置
quat quaternion 四元数
jac / J Jacobian 雅可比矩阵
FK Forward Kinematics 正运动学
IK Inverse Kinematics 逆运动学
DLS Damped Least Squares 阻尼最小二乘
TCP Tool Center Point 工具中心点

代码结构建议

"""模块文档字符串:这个文件做什么,怎么运行。"""
import numpy as np          # 1. 导入(标准库 → 第三方 → 本地)

# 2. 常量
IK_DAMPING = 0.02
MAX_ITER = 300

# 3. 类和函数
class IKSolver:
    """逆运动学求解器。"""
    def __init__(self, model, data, ee_site_id):
        ...

    def solve(self, target_pos, initial_q=None):
        """求解逆运动学。"""
        ...

# 4. 主程序(只有直接运行时才执行)
if __name__ == "__main__":
    solver = IKSolver(...)
    result = solver.solve(...)

💡 if __name__ == "__main__": 的作用:当你直接运行这个文件时,里面的代码会执行;当别的文件 import 这个文件时,里面的代码不会执行。这是 Python 的标准写法。


1.15 调试技巧

机器人代码出 bug 时,用这些方法快速定位:

1. 打印中间变量

# ❌ 出问题了,不知道哪一步错了
result = complex_calculation(a, b, c)

# ✅ 每一步都打印
print(f"a = {a}, shape = {a.shape if hasattr(a,'shape') else 'scalar'}")
print(f"b = {b}")
step1 = first_step(a)
print(f"step1 = {step1}")
step2 = second_step(step1, b)
print(f"step2 = {step2}")

2. 用断言(assert)检查假设

# 检查关节角是否在限位内
assert q.min() >= q_min and q.max() <= q_max, f"关节角超限: {q}"

# 检查雅可比形状
assert jac.shape == (3, 6), f"雅可比形状不对: {jac.shape}"

# 检查末端位置不是 NaN
assert not np.any(np.isnan(ee_pos)), "末端位置包含 NaN!"

3. 交互式调试(最强大)

在代码里加一个断点,运行后进入交互模式:

# 在出问题的地方加这行
import pdb; pdb.set_trace()

# 运行到这里会暂停,你可以:
# p variable    → 打印变量
# n             → 执行下一行
# s             → 进入函数
# c             → 继续运行
# q             → 退出

💡 Python 3.7+ 更简单:直接写 breakpoint() 就行,不用 import pdb


1.16 常见错误与排查

错误信息 原因 解决方法
IndentationError: unexpected indent 缩进不对(Python 用缩进表示代码块) 统一用 4 个空格,不要混用 Tab 和空格
TypeError: 'float' object is not subscriptable 对一个数字用了 [0](以为它是列表) 检查变量类型,print(type(x)) 确认
NameError: name 'np' is not defined 忘了 import numpy as np 在文件开头加上 import
IndexError: list index out of range 列表下标越界(比如 6 个元素却访问 [10] len() 检查长度,注意切片不含末尾
KeyError: 'J9' 字典里没有这个键 limits.get("J9")(不存在时返回 None)或 if "J9" in limits
AttributeError: 'numpy.ndarray' object has no attribute 'append' NumPy 数组没有 append 方法(和 list 不同) np.append(arr, value) 或直接用 list

💡 调试技巧:遇到报错时,在报错行前面加 print(type(变量), 变量),确认变量的类型和值是否符合预期。这是最快的定位方法。


1.17 扩展阅读方向

  • Python 官方教程:https://docs.python.org/zh-cn/3/tutorial/ (最权威的入门教程)
  • 《Python Crash Course》:适合零基础的入门书,前 10 章覆盖了本章全部内容
  • Real Python:https://realpython.com/ (大量高质量的 Python 教程,英文)
  • Python 之禅:在 Python 交互模式输入 import this,可以看到 Python 的设计哲学

1.18 小结

  • Python 变量不用声明类型,直接赋值;类型转换用 int()float()str()
  • 列表用数字下标(a[0]),字典用名字(a["J2"])——机器人里两种都常见。
  • 切片 a[start:stop] 不含 stop,这是最容易记错的点。
  • 类是本书最重要的语法:三个框架全由 ChainMjModelTask 这样的类构成。
  • self 是对象自己,定义时写、调用时不传__init__ 是构造方法,创建对象时自动调用。
  • 函数 = 封装重复逻辑;输入是参数,输出是 return 值。
  • f-string f"{x:.3f}" 是格式化输出的首选,:.3f 表示保留 3 位小数。
  • 列表推导式 [x*2 for x in lst] 比手写循环更简洁。
  • try/except 用于处理可选依赖等"可能失败"的情况。

⚠️ 全书通用约定:角度计算一律用弧度,只在打印给用户时转角度。


上一章:00 · 导言与环境准备 | 下一章:02 · 三维空间数学

Logo

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

更多推荐