乌龙出洞

没上过计算机系基础课程导致的。

Basic OOP

继承:暴露原有类的所有接口;如果 A 继承了 B,则默认 A 是一种特殊的 B。

组合:仅仅持有原有类的一份样本,并可以自由选择把原有类的哪些接口暴露出去;如果 A 组合了 B,意味着我们认为 A 是 A 本身,只不过恰好复用了一部分 B 的代码。

如果只是为了复用代码,应优先使用组合,此乃「组合优于继承」的哲学。

Variables

在 C++ 中,变量是对一块内存的操纵;而在 python 中,有一些非常神秘的逻辑。

  • 所有的 intfloatstrtuple 等常规类型,一旦被创建即不可修改。

  • a = 3 的效果是新创建或复用了一个 3,然后贴上 a 的标签。

  • 如果下一行是 a = 4,那效果是把 a 贴到新建或复用的 4 上面。

  • b = a 是令标签 b 和标签 a 指向同一个对象,没有拷贝。

  • 与之相对的,ListDictSet 是可变的,允许在不改变内存地址的前提下使用 append+= 等方法改变内部元素。但是如果你坚持使用 =,还是会开辟新的内存。

  • 深拷贝必须使用 copy.deepcopy

  • 非常神秘的逻辑:对于以下代码:

    a = [[1,2],3]
    b = a
    b[0].append(4)
    b[1] = 5
    print(a)
    

    则它会输出 [[1,2,4],3],因为 a[0]b[0] 会指向同一个可变对象,而 b[1] = 5 会让 b[1] 指向另一个不可变对象 5。相反,如果把第三行替换为 b[0] = b[0] + [4],则会输出 [[1,2],3],因为此时 b[0] 指向另一块地址了。

  • == 会调用内部方法 __eq__ 判断是否相等,而 is 判断是否指向同一块地址。判断是否为空必须用 is None,因为 None 是唯一的空地址。

  • 函数传参相当于给传入的东西临时贴了一些标签,和前述一样,in-place 的 append 等会修改传入内容,而 off-place 的 = 则不会。

  • def 中写入的默认值,会在 这一行 def 被读取时 被初始化,而不是 c++ 一样在每次被执行时初始化。在每次被执行时,会重新把默认值指向初始化时得到的地址。因此,如果使用 append 这种原地修改,则相当于修改了默认值。因此最有效的默认值设置方法,应当是默认为 None 然后用 is None 在内部进行默认值设置。

Comprehension

简洁的压行方案。

  • [expression for item in iterable (if condition)]:生成一个 List,其中的每一项是对 for item in iterable 中的 item 进行 expression 后的结果;用 if 添加一个 condition 是可选项。
  • {key_expression: value_expression for item in iterable if condition}。同上,这次是生成 Dict,其中的每一对 key_expression: value_expression 由满足条件的 iteration 中的 item 得到。
  • {expression for item in iterable if condition}。同上,这次是元素不重复的 Set
  • (expression for item in iterable if condition):相当于把 for item in iterable if condition 这个条件 iteration 套了一层 expression 的皮,得到一个新 iteration。比起第一条的优势是更省内存:其中的每个 item 只在被遍历到时才会创建。一个语法糖:如果它是某函数的唯一输入,则不需要两层括号。

同时,上述所有 for 都支持多重循环,且结合顺序是从左往右;也支持多重 if。但如果过长,一般还是别压行了。

Omits

既然提到了,梳理一些支持省略的语法糖。

  • 正如上一个例子,若生成器是函数的唯一参数,则可以省略括号。
  • 在赋值 x, y = y, x、返回 return x, y 或遍历 for x, y in iterable 时,可以省略元组的圆括号。
  • ifwhile 在处理输入时,会优先检查是否定义了 __bool__ 方法,如果有则调用;如果没有,则检查是否定义了 __len__ 方法,如果有则判断是否非零;都没有的话,按照是否为默认零值(比如说变量零、空序列等)判定。

Functional Programming

  • lambda:语法是 lambda args: expression
  • map:语法是 map(func, iterable),效果是把 func 作用于 iterable 的每一项,返回一个迭代器;如果要的是 List,需要显式转换。可以对多个序列同时处理,以支持多参数。等价于 (func(item) for item in iterable)
  • filter:语法是 filter(cond, iterable),只保留 cond 为真的所有 term。返回迭代器,如果要 List 需要显式转换。等价于 (item for item in iterable if cond(item))

其中,后两者也可以用推导式替代。

@

@decorator
def func(...):

等价于 func = decorator(func)。一个通用的 wrapper 一般长这样

def logger(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ...
        result = func(*args, **kwargs)
        ...
        return result
    return wrapper

一些常用装饰器:

@property:把一个不接受额外参数的成员函数伪装成一个成员变量。

  • 在已经使用 @property 转化一个示例函数 def example(self) 后,可以用 @example.setter 修饰 def example(self,value),定义这个伪装的成员变量在赋值时的行为;
  • @example.deleter 修饰 def example(self),定义这个伪装的成员变量在删除时的行为。

@staticmethod:定义静态方法,也即第一个参数不用是 self 的方法。

@classmethod:定义类方法,也即第一个参数要求是 cls 也即 type(self) 的方法。

@dataclass:作用于一个 class 外面,表明这个 class 是用来存储数据的(比如说 config),会自动把很多 dunder methods 配置好。只需要写

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

这样的写法,就可以自动用 User("Alice", 25) 这种方式创建实例。通过定义 __post_init__ 方法,可以进行自动校验。

Dunder Methods

将自定义 python 对象伪装成系统原生对象的方法。

  • __init__(self, *args):初始化。

  • __str__(self):决定 print(obj) 的输出。

  • __repr__(self):决定 debug 或在交互式终端敲变量名回车时的输出。

  • __len__(self):决定 len(obj) 的输出。

  • __getitem__(self, key):允许访问键值。

  • __setitem__(self, key, value):允许给键值赋值。

  • __next__(self) & __iter__(self)

    • for x in obj: ... 的本质是

      _iterator = obj.__iter__()
      while True:
          try:
              item = next(_iterator)
          except StopIteration:
              break
          ...
      
    • __next__ 的效果是指定 next(obj) 的返回值。

    • 另一种指定 next(gen) 返回值(前提是 gen 通过 gent = obj() 等方法被定义为一个生成器函数 )的技巧是在 obj 内部使用 yield 关键词,这会让每次 next(gen) 时,执行直到下一次 yield,然后暂停并保存上下文。

    • 于是一种实现遍历的技巧是 __iter__ 返回自身而 __next__ 处理后继,这个比较麻烦。

    • 而另一种实现遍历的技巧是直接在 __iter__yield

  • __contains__(self, obj):决定 item in objTrue 还是 False

  • __add__ 等:运算符重载。

  • __call__(self, *args):允许该对象本身以函数被调用,在 obj(*args) 被调用。

argparse

python 的一个库,用来解析 CLI 参数。

  • parser = argparse.ArgumentParser() 创建一个解析器。
    • 可以指定参数 descriptionepilog,用来控制 -h 时的输出。
  • parser.add_argument(...) 注册一个参数。
    • parser.add_argument("square") 这种不加 --- 的模式表明是位置参数,而且是必要的。
    • parser.add_argument("-v", "--verbose") 这种模式表明是可选参数,其中单 - 是简写,-- 是全称。
    • type 可以指定类型。
    • default 可以指定默认值。
    • choices 可以限制取值范围。
    • required = True 可以强制必须输入。
    • dest 可以定义在 python 中解析后该参数的访问名称。
    • nargs 可以指定这一项需要多少个参数(会被转为 List)。
    • action 用来指定行为:
      • action = "store_true" / "store_false" 表示如果加该标签则视作 True / False,不加则相反。
      • action = "count" 表示统计出现次数。
      • action = "append" 表示将多次重复置于同一个 List 中。
  • args = parser.parse_args() 会进行解析并得到一个类型为 argparse.Namespace 的示例。它的属性名的访问方式为:
    • 位置参数取其命名:add_argument("count") -> args.count
    • 可选参数取其长命名:add_argument("-v", "--verbose") -> args.verbose
    • 连字符参数会从 kebab-case 变成 snake_case:add_argument("--user-name") -> args.user_name
    • 如果有 dest 自定义,优先取用 dest 指定的目标:add_argument("-f", dest="file_path") -> args.file_path

torchrun

处理多卡训练的库。常见格式是 torchrun (--torchrun-args) train.py (--train-args)

通过 torchrun 启动时,每个进程的 os.environ 中会自动包含以下变量。这是实现分布式逻辑(如 Rank 分配)的基石。

变量名 含义 常用场景
RANK 全局唯一编号。范围 0WORLD_SIZE - 1 区分 Master (Rank 0) 以保存日志/模型检查点
LOCAL_RANK 当前机器内的唯一编号。通常是 0NPROC_PER_NODE - 1 指定当前进程使用哪张显卡 torch.cuda.set_device(local_rank)
WORLD_SIZE 全局总进程数(总 GPU 数) 计算全局 Batch Size,或进行 Data Parallel 的梯度缩放
LOCAL_WORLD_SIZE 当前机器内的总进程数 了解节点内的 GPU 密度
GROUP_RANK 节点(机器)的编号。从 0 开始 在多机训练时区分不同的物理机器
ROLE_RANK 弹性角色编号(一般等同于 RANK) 复杂弹性任务中使用
MASTER_ADDR 主节点的 IP 地址 内部通信建立,用户通常不需手动调用
MASTER_PORT 主节点的端口 内部通信建立

单机多卡的常见配置:

  • --nproc_per_node: 设置使用的 GPU 数量(通常等于该机显卡总数)。

多机多卡时,需要指定一台机器作为 master node,并额外提供以下配置:

  • --nnodes: 总机器数。
  • --rdzv_id: 任务 ID,同一作业的所有节点必须一致。
  • --rdzv_backend: 通信后端,通常用 c10d (Static) 或 etcd (Dynamic)。
  • --rdzv_endpoint: Master 节点的 IP 和端口,例如 10.0.0.1:29500

在 python 中,一般使用 world_size = int(os.environ["WORLD_SIZE"]) 等方式获取 torchrun 注入的信息。

当某个 local_rank 进程崩溃(如 Out of Memory)时,torchrun 会立即发送信号终止该任务下所有的其他进程,并根据 --max_restarts 的设置,重新尝试拉起所有进程。重启后所有 Rank 依然能正确对应其物理位置。

posted @ 2026-07-15 15:34  Troverld  阅读(25)  评论(0)    收藏  举报