python补习

装饰器

函数装饰器

什么是装饰器

装饰器是python的一种高级语法,本质上是函数包装器,可以在不修改函数代码的前提下为函数添加额外功能如日志记录、性能计时、权限校验,也可以修改函数的输入和输出。装饰器通过@装饰器名语法应用与函数,也是一种语法糖,简化包装的代码。

基本语法与原理

装饰器是一个接收函数作为参数,并返回新函数的函数。当用@decorator修饰函数func时,相当于执行func=decorator(func),即原函数被替换为装饰器返回的新函数。

# 定义装饰器:打印函数调用信息
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")  # 额外功能:打印日志
        result = func(*args, **kwargs)      # 执行原函数
        print(f"函数 {func.__name__} 执行完毕")
        return result                       # 返回原函数结果
    return wrapper

# 应用装饰器
@log_decorator
def add(a, b):
    return a + b

# 调用函数
add(1, 2)

上面打印的输出为

调用函数: add
函数 add 执行完毕
3

可以看到,相当于给add函数做了一层包装。这里 log_decorator 为 add 函数添加了“调用日志”功能,原 add 函数代码未做任何修改。

带参数的装饰器

如果装饰器需要自定义参数(如 @parser.wrap() 中的空括号),需在基础装饰器外再嵌套一层参数接收函数

# 带参数的装饰器:自定义日志前缀
def log_decorator(prefix="LOG"):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] 调用函数: {func.__name__}")  # 使用装饰器参数
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# 应用装饰器(传递参数)
@log_decorator(prefix="DEBUG")
def multiply(a, b):
    return a * b

multiply(3, 4)  # 输出: [DEBUG] 调用函数: multiply → 返回 12

装饰器在 Python 中非常常用,典型场景包括:

  • 日志记录:自动记录函数调用、参数、返回值;
  • 性能计时:统计函数执行时间;
  • 权限校验:检查用户是否有权限调用函数;
  • 输入/输出处理:自动转换参数类型、格式化返回值;
  • 资源管理:自动打开/关闭文件、数据库连接等。

类装饰器

前面描述了函数装饰器,还有类的装饰器。类的装饰器是通过修改类的定义、添加/覆盖方法或属性,或返回一个新类,来增强类的功能,与函数装饰器(修饰函数)不同,类装饰器专注于修饰类本身。类装饰器通过 @装饰器名 语法应用于类,是一种“语法糖”,简化了类的动态修改逻辑。

类装饰器是一个接收类作为参数,并返回新类的函数。当用 @decorator 修饰类 MyClass 时,相当于执行 MyClass = decorator(MyClass),即原类被“替换”为装饰器返回的新类。

简单类装饰器示例

# 定义类装饰器:为类添加一个属性和方法
def add_greeting(cls):
    cls.greeting = "Hello from decorator!"  # 新添加类属性

    def say_hello(self):  # 新定义要添加的实例方法
        return f"{self.greeting} I'm {self.name}."

    cls.say_hello = say_hello  # 将方法绑定到类
    return cls  # 返回修改后的类

# 应用装饰器
@add_greeting
class Person:
    def __init__(self, name):
        self.name = name

# 使用装饰后的类
p = Person("Alice")
print(p.greeting)       # 输出:Hello from decorator!
print(p.say_hello())    # 输出:Hello from decorator! I'm Alice.

内置装饰器@dataclass

@dataclass 是 Python 标准库 dataclasses 提供的内置类装饰器,用于快速定义数据存储类(Data Class),核心作用是:

  • 自动生成 init 方法:无需手动编写 def init(self, robot, dataset, ...): ...,装饰器会根据类字段自动生成。
  • 自动生成 repreq 等方法:方便打印实例(如 print(cfg))和比较实例是否相等。
  • 支持字段默认值和类型注解:如 teleop: TeleoperatorConfig | None = None 中的默认值和类型约束。

先来看看如果不适用@dataclass装饰器,定义一个普通的类,需要手动编写initrepr等方法,如下

class DatasetConfig:
    def __init__(self, repo_id: str, num_episodes: int = 50):
        self.repo_id = repo_id  # 手动绑定 self.xxx
        self.num_episodes = num_episodes
    def __repr__(self):  # 手动编写打印逻辑
        return f"DatasetConfig(repo_id={self.repo_id!r}, num_episodes={self.num_episodes!r})"

# 使用
cfg = DatasetConfig("aliberts/record-test", 2)
print(cfg)  # 输出:DatasetConfig(repo_id='aliberts/record-test', num_episodes=2)

如果使用了@dataclass,则不需要编写init等方法,但是需要添加属性的类型注解声明,如下面repo_id,num_episodes。如果有默认值的,如下的int=50,可选传递参数,如果没有默认值的,必现要传递参数如repo_id。

from dataclasses import dataclass

@dataclass
class DatasetConfig:
    repo_id: str  # 必选字段(无默认值)
    num_episodes: int = 50  # 可选字段(默认值 50)

# 使用(效果与普通类完全一致)
cfg = DatasetConfig("aliberts/record-test", 2)
print(cfg)  # 自动生成 __repr__:DatasetConfig(repo_id='aliberts/record-test', num_episodes=2)

@dataclass提供初始化后回调方法,在init执行完毕后自动调用,用于字段校验,动态修改字段值等。

from dataclasses import dataclass

@dataclass
class DatasetConfig:
    repo_id: str  # 必选字段(无默认值)
    num_episodes: int = 50  # 可选字段(默认值 50)

    def __post_init__(self):
        if self.repo_id is None:
            raise ValueError("You need to provide a repo_id as argument.")

函数返回类型注解

函数返回类型注解是python 3.0+引入的类型提示语法,用于显式声明函数预期返回值的类型。它不会改变函数的运行逻辑,只是为了提升代码的可读性、支持IDE智能提示,便于静态代码检查工具检测潜在错误。其语法格式为如下:

def 函数名(参数: 参数类型) -> 返回值类型:
    # 函数逻辑
    return 返回值

返回类型注解通过->类型语法声明,位于函数定义参数列表之后、冒号:之前。

def record(cfg: RecordConfig) -> LeRobotDataset:
    # ... 函数逻辑 ...
    return dataset  # dataset 是 LeRobotDataset 实例

这里 -> LeRobotDataset 表示:record 函数执行完毕后,预期返回一个 LeRobotDataset 类的实例。

注解都有哪些类型了,除了基础的int、float、str、bool、None(空)这几个类型外,还有容器类型、组合类型、特殊类型等。

容器类型

列表,list[值类型] ,用于标注列表、字典、元组等容器的元素类型,Python 3.9+ 支持直接用 list[int] 形式,旧版本需从 typing 模块导入(如 List[int])。

motor_names: list[str] = ["shoulder_pan", "elbow_flex"]  # 字符串列表
positions: list[float] = [0.2, 0.5, -0.3]  # 浮点数列表

字典,dict[键类型, 值类型]如下示例

def _motors_ft(self) -> dict[str, type]:  # 键为字符串,值为类型对象(如 float)
    return {f"{motor}.pos": float for motor in self.bus.motors}

元组,tuple[类型1, 类型2, ...],如下示例:

def _cameras_ft(self) -> dict[str, tuple]:  # 值为元组(高、宽、通道数)
    return {cam: (height, width, 3) for cam in self.cameras}
# 更精确标注:tuple[int, int, int](高、宽、3通道)

集合类型,set[元素类型],如下示例唯一电机ID集合。

motor_ids: set[int] = {1, 2, 3, 4, 5, 6}  # 整数集合

组合类型

Union,Union[类型1, 类型2, ...],允许整数或字符串的参数

from typing import Union

def get_motor(motor_id: Union[int, str]) -> Motor:  # motor_id 可为 int 或 str
    ...
# Python 3.10+ 简写:
def get_motor(motor_id: int | str) -> Motor:
    ...

Option,Optional[类型],(等价于 Union[类型, None]),可能为None的配置参数。

from typing import Optional

def connect(port: Optional[str] = None) -> None:  # port 可为字符串或 None
    ...

特殊类型

Any,任意类型,关闭类型检查,允许任何类型(常用于动态数据,如lerobot代码中 get_observation 返回 dict[str, Any])

from typing import Any

def get_observation(self) -> dict[str, Any]:  # 值可为任意类型(电机位置/图像等)
    ...

Callable,Callable[[参数类型1, 参数类型2], 返回值类型],接受函数作为参数。

from typing import Callable

def calibrate(callback: Callable[[str], None]) -> None:  # callback 是 (str) -> None 的函数
    callback("Calibration done")

type,Type[类型](标注“类型本身”而非实例,如lerobot代码中 dict[str, type]),接受类作为参数。

from typing import Type

def create_robot(robot_class: Type[Robot]) -> Robot:  # robot_class 是 Robot 的子类
    return robot_class()

配置选择注册机制

以draccus.ChoiceRegistry为例说明,draccus.ChoiceRegistry 是 draccus 配置框架提供的子类注册与动态选择机制。它允许将基类的多个子类注册为“可选选项”,并通过配置参数(如命令行、配置文件)动态选择具体子类。在工程中,这一机制用于实现 “同一接口,多种实现” 的灵活配置(例如不同机器人型号共享 RobotConfig 接口,但有各自的硬件参数实现)。

注册与选择流程

1. 基类:继承ChoiceRegistry并声明接口。如示例基类(如 RobotConfig)继承 draccus.ChoiceRegistry,作为所有子类的“公共接口”。它定义通用字段和方法,不包含具体实现细节。

from dataclasses import dataclass
import abc
import draccus

@dataclass(kw_only=True)
class RobotConfig(draccus.ChoiceRegistry, abc.ABC):  # 继承 ChoiceRegistry
    # 通用字段(所有子类共享)
    id: str | None = None  # 机器人标识
    calibration_dir: Path | None = None  # 校准文件路径

    @property
    def type(self) -> str:
        # 获取当前子类的注册名称(核心方法)
        return self.get_choice_name(self.__class__)

2. 子类:注册为可选项。每个具体实现(如不同机器人型号)定义一个 RobotConfig 的子类,补充特有字段和逻辑。draccus 会自动将子类注册为一个可选选项,如下:

例如,so101_follower 机器人的配置子类:

# SO101FollowerConfig(so101_follower 型号)
@dataclass(kw_only=True)
class SO101FollowerConfig(RobotConfig):
    port: str  # 型号特有字段(通信端口)
    disable_torque_on_disconnect: bool = True  # 型号特有字段(扭矩控制)

# KochFollowerConfig(koch_follower 型号)
@dataclass(kw_only=True)
class KochFollowerConfig(RobotConfig):
    ip_address: str  # 型号特有字段(以太网通信地址)
    timeout_ms: int = 500  # 型号特有字段(通信超时)

SO101FollowerConfig/KochFollowerConfig继承了RobotConfig,而RobotConfig继承了draccus.ChoiceRegistry。

3. 动态选择:通过配置参数指定子类。用户通过配置参数(如命令行 --robot.type=so101_follower)指定要使用的子类。draccus 会:

  • 根据参数值(so101_follower)查找注册的子类;
  • 实例化该子类,并将其他配置参数(如 --robot.port=/dev/ttyACM1)映射到子类字段;
  • 返回实例化后的子类对象,作为业务逻辑的输入。
# 命令行参数示例
python -m lerobot.record \
  --robot.type=so101_follower \  # 选择 SO101FollowerConfig 子类
  --robot.id=black \             # 设置通用字段 id
  --robot.port=/dev/ttyACM0 \    # 设置型号特有字段 port
  --robot.disable_torque_on_disconnect=true  # 设置型号特有字段

动作先行思维

C语言的风格是写法是条件先行,再写动作。而python支持动作先行写法,再补充条件,主要是为了简写。请看下面示例。

A if cond else B

# 简洁写法(条件表达式)
teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None

# 等价传统写法(if-else 块)
if cfg.teleop is not None:
    teleop = make_teleoperator_from_config(cfg.teleop)
else:
    teleop = None

使用的简洁方法是: A if cond else B。

or短路运算

# 例:若 config_path 为空,则默认使用 "./config.json"
config_path = user_provided_path or "./config.json"

for循环

C 的 for 循环强调“初始化→条件→增量”的控制流,而 Python 的 for 更关注“迭代对象→元素处理”,动作(循环体)直接跟在迭代逻辑后。

# 例:遍历数据集并处理每个帧
for frame in dataset.frames:
    process_frame(frame)  # 动作(循环体)直接跟在迭代逻辑后

Python 无需显式初始化索引、判断终止条件或手动增量(如 i++),迭代逻辑由“可迭代对象”(如列表、字典、生成器)内部处理。

列表推导式

Python 的列表推导式将“对元素的处理动作”放在最前面,直接表达“要生成什么样的列表”,而非 C 中“如何生成列表”的步骤式逻辑。

# 例:筛选偶数并计算平方(动作:x**2,条件:x
even_squares = [x**2 for x in range(10) if x 
# 结果:[0, 4, 16, 36, 64]

结构拆解:
- 动作(x**2):定义每个元素的转换方式(先明确“要做什么”);
- 迭代逻辑(for x in range(10)):从哪里获取元素;
- 条件(if x

字典推导式

核心逻辑是:{新键: 新值 for 键, 值 in 迭代器 if 条件}

先定义“键和值的生成动作”,再说明迭代范围和筛选规则,适用于快速构建字典。示例:将遥操作器原始动作(如 {"shoulder": 0.2, "gripper": 0.9})转换为带前缀的数据集格式:

# 原始遥操作动作
teleop_action = {"shoulder": 0.2, "elbow": 0.5, "gripper": 0.9}

# 字典推导式:先定义键值动作(添加前缀),再迭代
dataset_action = {
    f"action.{key}": value  # 动作:键添加前缀,值保持不变
    for key, value in teleop_action.items()  # 迭代范围:遥操作动作字典
    if key != "gripper"  # 筛选条件:排除 gripper(假设无需记录)
}


print(dataset_action) 
# 输出:{"action.shoulder": 0.2, "action.elbow": 0.5}

再来看看几个例子:

    features = {}
    joint_fts = {key: ftype for key, ftype in hw_features.items() if ftype is float}


遍历 hw_features 的所有键值对(key, ftype),仅保留 值为 float 类型 的键值对(即电机角度等浮点型特征)。若hw_features 含 {"shoulder.pos": float, "camera": (480,640,3)},则 joint_fts 为 {"shoulder.pos": float}。函数的作用就是筛选电机特征。实际打印如下:

joint_fts : {'shoulder_pan.pos': <class 'float'>, 'shoulder_lift.pos': <class 'float'>, 'elbow_flex.pos': <class 'float'>, 'wrist_flex.pos': <class 'float'>, 'wrist_roll.pos': <class 'float'>, 'gripper.pos': <class 'float'>}
    cam_fts = {key: shape for key, shape in hw_features.items() if isinstance(shape, tuple)}

遍历 hw_features,仅保留 值为元组类型 的键值对(即相机尺寸等元组特征,如 (高, 宽, 通道数))。示例:若 hw_features 含 {"camera": (480,640,3)},则 cam_fts 为 {"camera": (480,640,3)}。函数的作用就是筛选相机特征。

cam_fts : {'handeye': (480, 640, 3), 'fixed': (480, 640, 3)}
Action Features: {'action': {'dtype': 'float32', 'shape': (6,), 'names': ['shoulder_pan.pos', 'shoulder_lift.pos', 'elbow_flex.pos', 'wrist_flex.pos', 'wrist_roll.pos', 'gripper.pos']}}