python补习
- 其他
- 3天前
- 23热度
- 0评论
装饰器
函数装饰器
什么是装饰器
装饰器是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, ...): ...,装饰器会根据类字段自动生成。
- 自动生成 repr、eq 等方法:方便打印实例(如 print(cfg))和比较实例是否相等。
- 支持字段默认值和类型注解:如 teleop: TeleoperatorConfig | None = None 中的默认值和类型约束。
先来看看如果不适用@dataclass装饰器,定义一个普通的类,需要手动编写init,repr等方法,如下
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']}}