乌龙出洞
没上过计算机系基础课程导致的。
Basic OOP
继承:暴露原有类的所有接口;如果 A 继承了 B,则默认 A 是一种特殊的 B。
组合:仅仅持有原有类的一份样本,并可以自由选择把原有类的哪些接口暴露出去;如果 A 组合了 B,意味着我们认为 A 是 A 本身,只不过恰好复用了一部分 B 的代码。
如果只是为了复用代码,应优先使用组合,此乃「组合优于继承」的哲学。
Variables
在 C++ 中,变量是对一块内存的操纵;而在 python 中,有一些非常神秘的逻辑。
-
所有的
int、float、str、tuple等常规类型,一旦被创建即不可修改。 -
a = 3的效果是新创建或复用了一个3,然后贴上a的标签。 -
如果下一行是
a = 4,那效果是把a贴到新建或复用的4上面。 -
b = a是令标签b和标签a指向同一个对象,没有拷贝。 -
与之相对的,
List、Dict、Set是可变的,允许在不改变内存地址的前提下使用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时,可以省略元组的圆括号。 if和while在处理输入时,会优先检查是否定义了__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 obj是True还是False。 -
__add__等:运算符重载。 -
__call__(self, *args):允许该对象本身以函数被调用,在obj(*args)被调用。
argparse
python 的一个库,用来解析 CLI 参数。
parser = argparse.ArgumentParser()创建一个解析器。- 可以指定参数
description和epilog,用来控制-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 |
全局唯一编号。范围 0 到 WORLD_SIZE - 1 |
区分 Master (Rank 0) 以保存日志/模型检查点 |
LOCAL_RANK |
当前机器内的唯一编号。通常是 0 到 NPROC_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 依然能正确对应其物理位置。

浙公网安备 33010602011771号