01_Python 基础速成
01 · Python 基础速成
这一章要解决什么问题:假设你只写过
print("hello"),怎么在最短时间内看懂机器人代码?本章不讲 Python 的全部,只讲读机器人代码时真正用得到的那部分。
配套代码:code/ch01_python_basics.py
本章学习目标
读完本章,你将能够:
- 读懂机器人代码中的基本语法:变量、列表、字典、循环、函数、类——这些是 MuJoCo/ikpy/dm_control 代码的"词汇表"。
- 理解类与对象的关系:能解释
model = mujoco.MjModel.from_xml_path(...)这行代码在做什么,self是什么意思。 - 掌握角度与弧度的转换:知道为什么机器人代码内部一律用弧度,以及如何在显示时转成角度。
- 使用 f-string 格式化输出:能打印出类似
EE=[0.300, 0.040, 0.180]这样的调试信息。 - 独立完成本章的 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 np、import 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() |
| 类 | Chain、MjModel、MjData、Task |
| 模块导入 | import mujoco、from dm_control import mjcf |
| 列表推导式 | 批量处理轨迹点 |
| try/except | 可选依赖(imageio)的优雅降级 |
1.13 动手练
-
关节限位检查器:写一个函数
check_limits(angles_deg, limits),接收角度列表和限位字典,返回超限的关节名。limits = {"J1": (-180,180), "J2": (-155,67)}。 -
角度转换:把
[0, 30, 45, 60, 90]转成弧度并打印,保留 4 位小数。 -
定义一个
JointState类:属性name、angle、velocity;方法to_dict()返回字典。创建 3 个关节对象并打印。 -
(挑战)轨迹采样:函数
sample_line(p0, p1, n)返回从p0到p1的n个等间距点(每个点是 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))。- 用下标
i从angles_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=1时n-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,这是最容易记错的点。 - 类是本书最重要的语法:三个框架全由
Chain、MjModel、Task这样的类构成。 self是对象自己,定义时写、调用时不传;__init__是构造方法,创建对象时自动调用。- 函数 = 封装重复逻辑;输入是参数,输出是
return值。 - f-string
f"{x:.3f}"是格式化输出的首选,:.3f表示保留 3 位小数。 - 列表推导式
[x*2 for x in lst]比手写循环更简洁。 try/except用于处理可选依赖等"可能失败"的情况。
⚠️ 全书通用约定:角度计算一律用弧度,只在打印给用户时转角度。
上一章:00 · 导言与环境准备 | 下一章:02 · 三维空间数学
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)