collections 常用数据结构总览¶
| 数据结构 | 作用 | 典型场景 |
|---|---|---|
deque |
双端队列 | BFS、滑动窗口 |
defaultdict |
带默认值的字典 | 分组统计、图结构 |
Counter |
计数器字典 | 频率统计 |
OrderedDict |
有序字典 | LRU缓存 |
namedtuple |
具名元组 | 结构化数据 |
ChainMap |
多字典合并 | 配置管理 |
算法和工程中最常用的是:
deque / defaultdict / Counter
一、deque(双端队列)¶
1 核心特点¶
deque 是 双端队列:
- 头尾插入删除 O(1)
- 比
list.pop(0)快很多
from collections import deque
q = deque()
q.append(1) # 右侧入队
q.appendleft(2) # 左侧入队
q.pop() # 右侧出队
q.popleft() # 左侧出队
2 典型使用场景¶
BFS(最常见)¶
from collections import deque
queue = deque()
queue.append((x,y))
while queue:
x,y = queue.popleft()
典型题:
- 腐烂的橘子
- 岛屿问题
- 图最短路径
滑动窗口¶
from collections import deque
window = deque()
for i in range(len(nums)):
while window and nums[window[-1]] < nums[i]:
window.pop()
window.append(i)
典型题:
- 滑动窗口最大值
二、defaultdict(带默认值字典)¶
1 核心特点¶
普通字典:
d = {}
d["a"] += 1 # 报错
defaultdict 会自动创建默认值。
from collections import defaultdict
d = defaultdict(int)
d["a"] += 1
输出:
{'a':1}
2 常见默认类型¶
| 类型 | 含义 |
|---|---|
int |
默认0 |
list |
默认空列表 |
set |
默认空集合 |
3 典型使用场景¶
统计¶
from collections import defaultdict
count = defaultdict(int)
for num in nums:
count[num] += 1
分组¶
groups = defaultdict(list)
for word in words:
groups[len(word)].append(word)
图结构(算法中非常重要)¶
graph = defaultdict(list)
graph[1].append(2)
graph[1].append(3)
表示:
1 -> [2,3]
常见题:
- 图 DFS
- 图 BFS
- 拓扑排序
三、Counter(计数器)¶
Counter 是 统计频率的字典。
from collections import Counter
nums = [1,1,2,3,3,3]
c = Counter(nums)
print(c)
输出:
{3:3, 1:2, 2:1}
常用操作¶
1 统计次数¶
c[3] # 3
2 找出现最多元素¶
c.most_common(1)
输出:
[(3,3)]
3 字符统计¶
Counter("banana")
输出:
{'a':3,'n':2,'b':1}
典型使用场景¶
Top K 问题¶
Counter(nums).most_common(k)
典型题:
- 前K高频元素
字符串统计¶
Counter(s) == Counter(t)
典型题:
- 字母异位词
四、OrderedDict(有序字典)¶
普通字典(Python3.7+)已经保持插入顺序,但 OrderedDict 仍用于 顺序控制。
典型用途:
LRU Cache
from collections import OrderedDict
cache = OrderedDict()
cache["a"] = 1
cache.move_to_end("a")
常见题:
- LRU缓存
五、namedtuple(结构化元组)¶
用于创建 带字段名的元组。
from collections import namedtuple
Point = namedtuple("Point", ["x","y"])
p = Point(1,2)
print(p.x)
print(p.y)
输出:
1
2
优点:
- 比 tuple 可读性高
- 比 class 轻量
六、算法刷题中最重要的三个¶
几乎 90% LeetCode + 面试 用到的是:
| 数据结构 | 使用场景 |
|---|---|
| deque | BFS |
| defaultdict | 图结构 |
| Counter | 频率统计 |
七、刷题建议(最重要)¶
看到题目时快速判断:
1 BFS¶
用:
deque
例:
- 腐烂橘子
- 最短路径
2 图结构¶
用:
defaultdict(list)
例:
- 拓扑排序
- 图DFS
3 频率统计¶
用:
Counter
例:
- 前K高频元素
- 字符统计
Python 变量作用域完整讲解¶
一、LEGB 规则:作用域的查找顺序¶
Python 在解析一个变量名时,会按照固定的四层顺序依次向外查找,这一规则被称为 LEGB:
Local(局部)→ Enclosing(外层函数)→ Global(模块全局)→ Built-in(内置)
Python 从最内层开始查找,一旦在某一层找到该变量名,便立即停止。若四层都未找到,则抛出 NameError。这四个层级覆盖了绝大多数日常开发场景,理解它是掌握所有作用域问题的基础。
二、读与写的不对等性:核心陷阱¶
作用域规则中最容易引发误解的地方在于,读取变量与写入变量遵循不同的规则。
读取时,Python 按 LEGB 顺序向外逐层查找,能找到就能用,不受层级限制。写入(即赋值 =)时则完全不同——Python 一旦在某个函数内部看到对某个变量名的赋值语句,便会将该变量名静态地标记为当前函数的局部变量,无论赋值语句写在函数的哪个位置。这意味着,如果你在赋值之前就试图读取它,Python 不会向外层查找,而是直接报 UnboundLocalError。
x = 10
def foo():
print(x) # UnboundLocalError:Python 看到下面有 x = ...,
x = 20 # 便将 x 标记为局部变量,导致上面的读取失败
三、打破默认规则的两个关键字¶
当确实需要在内层函数中对外层变量进行赋值时,Python 提供了两个关键字来显式声明意图。
global 用于声明"此变量名指向模块全局作用域中的那个变量",从而允许在函数内对其赋值。这是直接操作模块级变量的标准方式,但在大型工程中应谨慎使用,因为它会引入隐式的全局状态耦合。
nonlocal 用于嵌套函数场景,声明"此变量名指向最近一层外层函数中的那个变量",允许对其赋值。这是解决闭包中整数、布尔等不可变类型回写问题的最直接方式,也是 self.变量名 挂载写法之外的另一个主流替代方案。
# global 示例
count = 0
def increment():
global count
count += 1 # 合法,修改的是模块全局的 count
# nonlocal 示例
def outer():
x = 0
def inner():
nonlocal x
x += 1 # 合法,修改的是 outer 作用域中的 x
inner()
return x # 返回 1
四、可变类型 vs. 不可变类型:对作用域的不同影响¶
这是将上述理论与实际编码习惯衔接起来的关键一步。
对于不可变类型(整数、字符串、布尔值、元组),更新其值的唯一方式是重新赋值(x = x + 1),因此必然触发上述的写入规则,需要显式使用 global、nonlocal 或 self. 来声明。
对于可变类型(列表、字典、集合),在嵌套函数中调用 .append()、.pop()、[key] = value 等操作,属于对对象内部状态的原地修改,而非对变量名的重新绑定。Python 不会将这类操作视为赋值,因此它们可以直接穿透作用域边界生效,无需任何额外声明。
def outer():
nums = [] # 可变类型,无需 nonlocal
total = 0 # 不可变类型,需要 nonlocal
def inner(x):
nonlocal total
nums.append(x) # 原地修改,直接有效
total += x # 重新赋值,必须声明 nonlocal
inner(1); inner(2)
return nums, total # ([1, 2], 3)
五、self. 挂载:面向对象场景下的惯用解法¶
在类方法中定义嵌套函数时,self 本身是一个可变对象(类实例),因此对 self.变量名 的赋值实际上是在修改 self 这个对象的属性,属于"原地修改可变对象",而非对变量名的重新绑定。这就是为什么 self.max_diameter = ... 能在嵌套函数中直接生效,而无需声明 nonlocal。在算法题的类方法场景下,self. 挂载与 nonlocal 在效果上完全等价,选择哪种主要取决于代码风格偏好。
小结¶
| 场景 | 变量类型 | 操作 | 是否需要声明 |
|---|---|---|---|
| 嵌套函数中更新计数器 | 整数(不可变) | x += 1 |
需要 nonlocal 或 self. |
| 嵌套函数中向列表追加 | 列表(可变) | .append() |
不需要 |
| 普通函数中修改全局变量 | 任意 | 赋值 | 需要 global |
| 类方法的嵌套函数中更新结果 | 挂载于 self |
self.x = ... |
不需要 |
Python 对象模型与引用语义¶
一、Python 中"变量"的本质¶
许多编程语言(如 C/C++)中,变量是一块有名字的内存格子,赋值意味着将数据直接写入该格子。Python 的模型与此根本不同:Python 中的变量不存储值,而是存储指向对象的引用(即内存地址)。对象本身独立地存活在堆内存中,变量只是贴在它上面的一张标签。
执行 a = [1, 2, 3] 时,Python 做了两件事:在堆上创建一个列表对象,然后让变量名 a 指向它。执行 b = a 时,Python 并不创建新列表,而是让 b 也指向同一个对象。此后 a 和 b 是完全平等的两个标签,贴在同一块内存上。
a = [1, 2, 3]
b = a
print(a is b) # True —— 同一个对象,而非内容相等的两个对象
理解这一点,是理解 Python 中所有"赋值"、"传参"、"拷贝"行为的前提。
二、可变类型与不可变类型:两种截然不同的行为¶
Python 中的所有对象分为两类,其区别对引用语义的实际表现有决定性影响。
不可变类型(整数、字符串、布尔值、元组)的对象一旦创建,其内容永远无法改变。x = x + 1 并非修改原对象,而是创建了一个新的整数对象,并将 x 这个标签重新贴过去。因此,两个变量即使指向同一个不可变对象,也不会因为其中一方"修改"而相互影响——因为所谓的"修改"实际上是重新绑定,而非原地改变。
可变类型(列表、字典、集合、自定义类实例)的对象内容可以原地修改。当两个变量指向同一个可变对象时,通过任意一方所做的修改,都会被另一方同步观察到。这是 Python 中大量"意料之外"行为的根源。
# 不可变类型:互不影响
a = 10
b = a
b = b + 1
print(a) # 10 —— a 未受影响,b 只是贴到了新对象 11 上
# 可变类型:相互影响
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] —— a 和 b 指向同一对象,修改可见
三、函数传参:传递的始终是引用¶
Python 的函数传参机制有时被描述为"传值"或"传引用",但这两种说法都不够准确。正确的描述是:Python 传递的是对象引用的副本,有时也称为"传对象引用"(Pass by Object Reference)。
实际效果是:函数内部可以通过参数变量对可变对象进行原地修改,这些修改在函数外部可见;但若函数内部将参数重新绑定到一个新对象(即赋值),则不会影响外部的原始变量。
def modify(lst, num):
lst.append(99) # 原地修改可变对象 —— 外部可见
num = num + 1 # 重新绑定不可变对象 —— 外部不可见
my_list = [1, 2]
my_num = 10
modify(my_list, my_num)
print(my_list) # [1, 2, 99] —— 被修改
print(my_num) # 10 —— 未受影响
四、拷贝的三个层次¶
当需要独立的副本而非共享引用时,Python 提供了三种语义层次的拷贝方式。
直接赋值不是拷贝,只是新增一个指向同一对象的引用,如前文所述。
浅拷贝(Shallow Copy) 创建一个新的容器对象,但容器内部的元素仍然是原始对象的引用。对于只有一层嵌套的列表,浅拷贝已经足够隔离;但若列表内部还包含可变对象(如嵌套列表),则内层对象仍然是共享的。常用方式包括 list[:]、list(original),以及 copy.copy()。
深拷贝(Deep Copy) 递归地复制对象及其所有嵌套的子对象,最终得到一个在内存上完全独立的副本。仅在处理多层嵌套的可变结构时才有必要使用,通过 copy.deepcopy() 实现。
import copy
original = [[1, 2], [3, 4]]
shallow = original[:] # 浅拷贝
deep = copy.deepcopy(original) # 深拷贝
original[0].append(99)
print(shallow[0]) # [1, 2, 99] —— 内层对象仍共享,受影响
print(deep[0]) # [1, 2] —— 完全独立,不受影响
在算法题中,path[:] 属于浅拷贝。由于 path 通常只存储整数(不可变类型),浅拷贝已能完全满足需求,无需动用深拷贝。
五、常见陷阱:函数默认参数¶
Python 中一个经典的高频陷阱,是将可变对象作为函数的默认参数值。默认参数对象在函数定义时创建一次,此后被所有调用共享,而非每次调用时重新创建。
def add_item(item, container=[]): # 危险写法
container.append(item)
return container
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] —— 并非 [2],默认列表被复用了
正确做法是将默认值设为 None,在函数体内显式创建新对象:
def add_item(item, container=None):
if container is None:
container = []
container.append(item)
return container
小结¶
| 概念 | 要点 |
|---|---|
| 变量的本质 | 标签(引用),而非存储值的格子 |
| 赋值操作 | 重新绑定标签,而非复制内容 |
| 不可变类型 | 无法原地修改,"更新"即创建新对象 |
| 可变类型 | 可原地修改,所有指向它的引用均可见变化 |
| 函数传参 | 传递引用的副本,可变对象的原地修改外部可见 |
| 浅拷贝 | 新容器 + 共享内层引用,list[:] 即可实现 |
| 深拷贝 | 完全独立副本,需 copy.deepcopy() |
| 默认参数陷阱 | 可变默认参数在所有调用间共享,应以 None 代替 |
这套对象模型是 Python 语言设计的基础哲学之一,理解它不仅能解释本文所有的"怪异"行为,更能帮助你在编写任何涉及数据传递与状态管理的代码时做出正确的判断。
Python 继承专题¶
继承的本质是什么?¶
继承的核心思想是:子类自动获得父类的所有属性和方法,并可以在此基础上扩展或覆盖。 它解决的问题是代码复用——把多个类共同需要的逻辑写在父类里,子类只写自己特有的部分。
用我们代码中的优化器举例:SGD 和 Adam 都需要持有 params 和 lr,都需要 zero_grad() 方法。与其在两个类里各写一遍,不如抽到父类 Optimizer 中,子类继承后直接使用。
# 优化器基类
class Optimizer:
def __init__(self, params, lr):
"""
params: 所有需要更新的参数列表,如 [w1, b1, w2, b2, ...]
"""
self.params = params
self.lr = lr
def step(self):
raise NotImplementedError
def zero_grad(self):
for p in self.params:
if p.grad is not None:
p.grad.zero_()
# SGD 优化器
class SGD(Optimizer):
def __init__(self, params, lr=1e-2, momentum=0.0):
"""
momentum: 动量系数,0 表示普通 SGD
动量公式:v = momentum * v - lr * grad
w = w + v
"""
super().__init__(params, lr)
self.momentum = momentum
# 为每个参数维护一个速度缓冲 v
self.velocity = [torch.zeros_like(p) for p in self.params]
def step(self):
with torch.no_grad():
for p, v in zip(self.params, self.velocity):
if p.grad is None:
continue
v.mul_(self.momentum).sub_(self.lr * p.grad) # v = momentum*v - lr*grad
p.add_(v) # w = w + v
# Adam 优化器
class Adam(Optimizer):
def __init__(self, params, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
"""
beta1: 一阶矩(梯度均值)的衰减系数
beta2: 二阶矩(梯度方差)的衰减系数
eps: 防止除零的小量
"""
super().__init__(params, lr)
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0 # 时间步,用于偏差修正
self.m = [torch.zeros_like(p) for p in self.params] # 一阶矩
self.v = [torch.zeros_like(p) for p in self.params] # 二阶矩
def step(self):
self.t += 1
# 偏差修正系数:早期 t 小时,m/v 被低估,需放大
bias_correction1 = 1 - self.beta1 ** self.t
bias_correction2 = 1 - self.beta2 ** self.t
with torch.no_grad():
for p, m, v in zip(self.params, self.m, self.v):
if p.grad is None:
continue
# 更新一阶矩:梯度的指数移动平均
m.mul_(self.beta1).add_((1 - self.beta1) * p.grad)
# 更新二阶矩:梯度平方的指数移动平均
v.mul_(self.beta2).add_((1 - self.beta2) * p.grad ** 2)
m_hat = m / bias_correction1 # 修正后的一阶矩
v_hat = v / bias_correction2 # 修正后的二阶矩
p.sub_(self.lr * m_hat / (v_hat.sqrt() + self.eps))
super().__init__(params, lr) 到底做了什么?¶
要理解这一行,需要先看父类的 __init__:
class Optimizer:
def __init__(self, params, lr):
self.params = params # 把参数列表绑定到 self 上
self.lr = lr # 把学习率绑定到 self 上
def zero_grad(self):
for p in self.params:
if p.grad is not None:
p.grad.zero_()
父类的 __init__ 做了两件事:将 params 和 lr 保存为实例属性。现在看子类:
class SGD(Optimizer):
def __init__(self, params, lr=1e-2, momentum=0.0):
super().__init__(params, lr) # 第一步:执行父类的 __init__
self.momentum = momentum # 第二步:再添加 SGD 特有的属性
self.velocity = [torch.zeros_like(p) for p in self.params] # ← 注意这里
super().__init__(params, lr) 这一行的作用是:显式调用父类的初始化方法,将 params 和 lr 传进去,让父类完成它该做的初始化工作。 执行完这一行之后,self.params 和 self.lr 就已经存在了,所以下一行的 self.params 才能正常访问。
传给 super().init() 的是值,父类用自己定义的属性名(例如params1。)来保存这个值。但子类之后访问时,必须用父类定义的属性名 self.params,而不是子类局部参数的名字 self.params1。
如果去掉这一行会发生什么:
class SGD(Optimizer):
def __init__(self, params, lr=1e-2, momentum=0.0):
# super().__init__(params, lr) 被注释掉
self.momentum = momentum
self.velocity = [torch.zeros_like(p) for p in self.params] # ❌ AttributeError
# self.params 从未被赋值,访问时直接报错
用一个完整的例子彻底说清楚¶
# ── 父类 ──────────────────────────────────────────────
class Animal:
def __init__(self, name, sound):
self.name = name # 所有动物共有:名字
self.sound = sound # 所有动物共有:叫声
def speak(self):
print(f"{self.name} says {self.sound}")
def describe(self):
print(f"I am {self.name}")
# ── 子类 A ────────────────────────────────────────────
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, sound="Woof") # 让父类完成 name 和 sound 的初始化
self.breed = breed # Dog 特有属性
def fetch(self):
print(f"{self.name} fetches the ball!") # self.name 来自父类,直接可用
# ── 子类 B ────────────────────────────────────────────
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, sound="Meow")
self.indoor = indoor
def speak(self): # 覆盖(override)父类方法
print(f"{self.name} says Meow softly~")
# ── 调用 ──────────────────────────────────────────────
dog = Dog("Rex", breed="Labrador")
dog.speak() # Rex says Woof ← 来自父类方法,未被覆盖
dog.fetch() # Rex fetches the ball! ← Dog 自己的方法
dog.describe() # I am Rex ← 来自父类方法
cat = Cat("Mimi")
cat.speak() # Mimi says Meow softly~ ← 使用覆盖后的版本
对应到你的优化器代码,结构完全一致:
| 概念 | 动物例子 | 优化器例子 |
|---|---|---|
| 父类 | Animal |
Optimizer |
| 子类 | Dog, Cat |
SGD, Adam |
| 父类共有属性 | name, sound |
params, lr |
| 子类特有属性 | breed, indoor |
velocity, m/v |
| 父类共有方法 | describe() |
zero_grad() |
| 子类覆盖方法 | Cat.speak() |
SGD.step(), Adam.step() |
super() 的括号里为什么有时有参数,有时没有?¶
这是最容易混淆的地方,规则很简单:super() 本身不传参数,但调用父类方法时需要传该方法所需的参数。
super().__init__() # 调用父类 __init__,且父类 __init__ 不需要额外参数
super().__init__(params, lr) # 调用父类 __init__,且父类 __init__ 需要 params 和 lr
super().some_method(x, y) # 调用父类任意方法,传入该方法需要的参数
super() 括号里为空,是 Python 3 的语法简化。Python 2 需要写 super(SGD, self).__init__(params, lr),Python 3 直接写 super().__init__(params, lr) 即可,含义相同。
最终整体结构回顾¶
Optimizer(父类)
├── __init__:保存 params, lr ← super().__init__(params, lr) 触发这里
├── zero_grad:清零所有参数梯度 ← 子类直接继承,无需重写
└── step:抽象接口,子类必须实现
SGD(子类)
├── __init__:先调父类初始化,再加 velocity
└── step:实现动量 SGD 的具体更新公式
Adam(子类)
├── __init__:先调父类初始化,再加 m, v, t
└── step:实现 Adam 的具体更新公式
总结一句话:super().__init__(params, lr) 的意思是"先让父类把公共的初始化工作做完,我再在此基础上添加自己特有的东西"。这是所有继承结构中 __init__ 的标准写法。
| 语法 | 作用 |
|---|---|
class Adam(BaseOptimizer) |
声明 Adam 继承 BaseOptimizer,获得其所有方法 |
super().__init__(params, lr) |
调用父类的 __init__,执行父类的初始化赋值操作 |
Adam 优化器中的 PyTorch 原地操作详解¶
一、核心概念:带下划线 _ = 原地操作(In-place)¶
# 普通操作:创建新张量,原张量不变
a = torch.tensor([1.0, 2.0])
b = a.mul(2) # b 是新张量,a 还是 [1, 2]
# 原地操作:直接修改原张量,不创建新对象
a.mul_(2) # a 本身变成 [2, 4],没有新张量产生
为什么优化器要用原地操作? 优化器管理的
m、v、p都是需要持久存活的状态缓冲,每次 step 只是更新其数值,不能丢掉原对象再新建一个——否则字典里存的引用就失效了。
二、逐行拆解 Adam 的核心更新¶
第一行:更新一阶矩 m
m.mul_(self.beta1).add_(g, alpha=1 - self.beta1)
对应数学公式:$m_t = \beta_1 \cdot m_{t-1} + (1-\beta_1) \cdot g_t$
拆开看:
m.mul_(self.beta1) # m = m * beta1 (衰减旧均值)
.add_(g, alpha=1 - self.beta1) # m = m + g*(1-beta1) (加入新梯度)
alpha 是 add_ 的缩放系数,等价于:
# 等价写法(但会产生临时张量,浪费内存)
m = m * self.beta1 + (1 - self.beta1) * g
第二行:更新二阶矩 v
v.mul_(self.beta2).addcmul_(g, g, value=1 - self.beta2)
对应数学公式:$v_t = \beta_2 \cdot v_{t-1} + (1-\beta_2) \cdot g_t^2$
addcmul_ = add + component-wise multiply(逐元素乘后相加):
# addcmul_(tensor1, tensor2, value=k) 等价于:
# self += k * tensor1 * tensor2
v.addcmul_(g, g, value=1-self.beta2)
# 即:v = v + (1-beta2) * g * g ← g*g 就是梯度平方
第三行:参数更新
p.addcdiv_(m_hat, v_hat.sqrt().add(self.eps), value=-self.lr)
对应数学公式:$\theta_t = \theta_{t-1} - \alpha \cdot \dfrac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$
addcdiv_ = add + component-wise divide(逐元素除后相加):
# addcdiv_(numerator, denominator, value=k) 等价于:
# self += k * (numerator / denominator)
p.addcdiv_(m_hat, v_hat.sqrt().add(self.eps), value=-self.lr)
# 即:p = p + (-lr) * m_hat / (sqrt(v_hat) + eps)
三、为什么不用 torch.matmul?¶
| 操作 | 适用场景 |
|---|---|
torch.matmul |
矩阵乘法,做线性变换(如 x @ W) |
mul_ / addcmul_ |
逐元素乘法,每个参数独立缩放 |
Adam 更新的本质是:对每个参数分量独立地计算自适应学习率,不同分量之间没有任何交叉影响,因此是逐元素操作,与矩阵乘法无关。
# 直觉类比:
# matmul: [1,2] @ [[3],[4]] = 1*3 + 2*4 = 11 ← 各分量混合
# mul_ : [1,2] * [3,4] = [3, 8] ← 各分量独立
四、完整操作速查表¶
操作符 含义 等价普通写法
─────────────────────────────────────────────────────────────────
a.mul_(k) a = a * k a *= k
a.add_(b, alpha=k) a = a + k*b a += k * b
a.addcmul_(b, c, value=k) a = a + k*(b*c) 逐元素 a += k * b * c
a.addcdiv_(b, c, value=k) a = a + k*(b/c) 逐元素 a += k * b / c
a.sqrt() 返回新张量(各元素开根号) —
a.add(eps) 返回新张量(各元素加 eps) —(注意无下划线)
链式调用如
v_hat.sqrt().add(self.eps)中没有下划线,是因为这里需要的是临时计算结果(分母),不需要修改v_hat本身。
五、一图总结 Adam 一次 step 的数据流¶
g = p.grad
│
├──→ m.mul_(β1).add_(g, α=1-β1) 更新一阶矩 m(梯度均值)
│
├──→ v.mul_(β2).addcmul_(g,g, v=1-β2) 更新二阶矩 v(梯度方差)
│
├──→ m_hat = m / (1 - β1^t) 偏差修正
│ v_hat = v / (1 - β2^t)
│
└──→ p.addcdiv_(m_hat, √v_hat+ε, v=-lr) 参数更新
Python 运算符专题讲解:面向算法竞赛初学者¶
适用对象:ACM / OJ 竞赛入门学习者
学习目标:系统掌握 Python 常见运算符及其在算法题中的典型应用
一、Python 常见算术运算符¶
Python 提供了七种基本算术运算符,每一种都在算法题中有其特定的应用场景。
1.1 运算符速览¶
| 运算符 | 英文名称 | 数学含义 | 典型应用场景 |
|---|---|---|---|
+ |
Addition | 加法 | 求和、字符串拼接 |
- |
Subtraction | 减法 | 差值计算、偏移量 |
* |
Multiplication | 乘法 | 组合计数、面积计算 |
/ |
True Division | 实数除法 | 几何计算、期望值 |
// |
Floor Division | 向下取整除法 | 分组、二分中点 |
% |
Modulo | 取余/取模 | 奇偶判断、循环数组、哈希 |
** |
Exponentiation | 幂运算 | 快速幂、多项式计算 |
1.2 逐一详解¶
+ 加法¶
a, b = 3, 5
print(a + b) # 8
print("AC" + "M") # 字符串拼接:ACM
竞赛场景:前缀和、区间求和、BFS 路径长度累加。
- 减法¶
print(10 - 3) # 7
print(abs(-5)) # 取绝对值:5
竞赛场景:差分数组、计算两点距离、偏移量转换(如坐标压缩)。
* 乘法¶
print(6 * 7) # 42
print([0] * 5) # 初始化列表:[0, 0, 0, 0, 0]
竞赛场景:组合计数、矩阵初始化、等差数列求和公式。
/ 实数除法(True Division)¶
print(7 / 2) # 3.5 ← 结果是浮点数
print(6 / 2) # 3.0 ← 注意:整数相除也是浮点数
竞赛场景:几何题(斜率、距离)、概率期望计算。
常见错误:将/误用于需要整数下标的场合,如arr[n / 2]会报错,应使用arr[n // 2]。
// 向下取整除法(Floor Division)¶
print(7 // 2) # 3 ← 向下取整
print(-7 // 2) # -4 ← 注意:负数向负无穷取整,不是截断
print(7 // -2) # -4
/ 与 // 的本质区别:
| 表达式 | 结果 | 类型 |
|---|---|---|
7 / 2 |
3.5 |
float |
7 // 2 |
3 |
int |
-7 // 2 |
-4 |
int(向负无穷取整) |
竞赛场景:二分法求中点
mid = (lo + hi) // 2;分组问题(n 个元素每组 k 个,共n // k组);树结构中父节点索引parent = i // 2(1-indexed 堆)。
常见陷阱:Python 的//是"向下取整(floor)",而 C/C++ 的整数除法是"向零截断(truncate)"。对负数而言两者结果不同,竞赛中需特别注意。
% 取余/取模¶
详见第二节(重点模块)。
** 幂运算¶
print(2 ** 10) # 1024
print(2 ** 0.5) # 1.4142135...(等价于 √2)
print(pow(2, 10, 1000000007)) # 快速幂取模:1024
竞赛场景:计算 $2^n$、矩阵幂;但在竞赛中强烈推荐使用内置
pow(a, b, mod)代替a**b % mod,前者效率更高且不会溢出中间值(详见第六节)。
常见错误:2**10**2的运算顺序是2**(10**2) = 2**100,因为**是右结合的。
1.3 记忆口诀¶
"加减乘除,整除取模,幂次方"
/是"真除"得小数,//是"整除"向下取,%是"余数"找规律,**是"幂次"快速算。
二、取余运算 % 与模数 mod 的关系¶
本节是竞赛算法的核心基础,务必深入理解。
2.1 余数(Remainder)与模数(Modulus)的定义¶
余数(Remainder):整数 a 除以 b 后"剩下的部分"。
数学表达:若 $a = b \times q + r$,则 $r$ 是 $a$ 除以 $b$ 的余数,其中 $0 \leq r < b$(b > 0 时)。
模数(Modulus):在 a % b 中,b 就是模数(即除数),a 是被除数。
a = 17
b = 5 # b 是模数
r = a % b # r = 2,即余数
# 验证:17 = 5 × 3 + 2
2.2 i % 2 对 2 取模:判断奇偶性¶
表达式 i % 2 的含义是:将 i 除以 2,取其余数。
由于任意整数除以 2,余数只能是 0 或 1,因此:
i % 2 == 0:i是偶数(被 2 整除,无余数)i % 2 == 1:i是奇数(不能被 2 整除,余 1)
为什么模 2 可以判断奇偶?
本质在于十进制整数的奇偶性完全由最低位决定。偶数最低位为 0(可被 2 整除),奇数最低位为 1(不可被 2 整除)。% 2 操作正是提取这一信息。
for n in [7, 8, -3, 0]:
if n % 2 == 0:
print(f"{n} 是偶数")
else:
print(f"{n} 是奇数")
# 输出:
# 7 是奇数
# 8 是偶数
# -3 是奇数
# 0 是偶数
print(7 % 2) # 1 → 7 是奇数
print(8 % 2) # 0 → 8 是偶数
注意:Python 中
-3 % 2 = 1(结果非负,符合数学模运算定义)。C++ 中-3 % 2 = -1(截断除法)。跨语言移植时需注意符号差异。
2.3 同余关系:a ≡ b (mod m) 的含义¶
数学符号 $a \equiv b \pmod{m}$ 表示:$a$ 和 $b$ 被 $m$ 除后余数相同,即 $m \mid (a - b)$($m$ 整除 $a - b$)。
# 17 ≡ 2 (mod 5),因为 17 % 5 = 2,且 2 % 5 = 2
print(17 % 5) # 2
print(2 % 5) # 2
print((17 - 2) % 5 == 0) # True,即 5 | 15
在竞赛中,这意味着凡是对 m 取模后结果相同的两个数,在"模 m 的意义下"是等价的——这是哈希、周期性分析、数论问题的基础。
2.4 竞赛中 % 的四大核心应用¶
应用 1:循环数组(Circular Array)¶
场景:在长度为 n 的数组中,下标 i 的下一个位置是 (i + 1) % n,自动回绕到开头。
n = 5
arr = [10, 20, 30, 40, 50]
# 模拟循环移动 k 步
i = 3
for _ in range(7): # 超过数组长度的步数
print(arr[i % n], end=" ")
i += 1
# 输出:40 50 10 20 30 40 50
本质:
% n将无限的整数序列映射到[0, n-1]的有限区间,实现"环形"效果。
应用 2:大数取模防溢出¶
场景:题目要求"答案对 $10^9 + 7$ 取模",防止中间计算结果超出整数范围。
Python 整数本身无溢出,但为遵循题目要求且保持高效:
MOD = 1_000_000_007
# 错误做法(中间值极大,对某些语言会溢出)
result = (a * b * c) % MOD
# 正确做法:每步取模,保持数值在合理范围
result = 1
result = result * a % MOD
result = result * b % MOD
result = result * c % MOD
数学保证:$(a \times b) \bmod m = ((a \bmod m) \times (b \bmod m)) \bmod m$,即取模操作对乘法具有分配律。
应用 3:快速幂(Fast Exponentiation)¶
场景:计算 $a^b \bmod m$,若用 a**b % m 则中间结果极大;应使用 Python 内置 pow(a, b, m),其内部实现即快速幂算法。
MOD = 1_000_000_007
# 计算 2^(10^9) mod (10^9+7)
result = pow(2, 10**9, MOD)
print(result) # 快速得出结果,不会溢出或超时
手动实现快速幂以理解原理:
def fast_pow(base, exp, mod):
result = 1
base %= mod
while exp > 0:
if exp % 2 == 1: # 当前位为奇数(二进制末位为 1)
result = result * base % mod
base = base * base % mod
exp //= 2
return result
print(fast_pow(2, 10, MOD)) # 1024
核心思想:利用 $a^{2k} = (a^k)^2$ 将指数每次减半,时间复杂度从 $O(b)$ 降至 $O(\log b)$。
exp % 2用于判断当前二进制位是否为 1,exp //= 2用于右移一位。
应用 4:字符串哈希(String Hashing)¶
场景:将字符串映射为整数,用于快速比较子串是否相等。
MOD = 1_000_000_007
BASE = 131
s = "abcdef"
h = 0
for c in s:
h = (h * BASE + ord(c)) % MOD # 每步取模,防止数值爆炸
print(h) # 字符串 "abcdef" 对应的哈希值
% MOD的作用:将哈希值限制在[0, MOD-1]范围内,既防止溢出,又将哈希表大小控制在合理区间。
2.5 记忆口诀¶
"b 是模数 a 被除,余数在 [0, b-1] 住;模 2 判奇偶,模 n 绕圆圈;乘法分配律,取模防溢出。"
三、比较运算符¶
比较运算符用于判断两个值之间的关系,返回布尔值 True 或 False。
3.1 运算符列表¶
| 运算符 | 英文名称 | 含义 | 示例 | 结果 |
|---|---|---|---|---|
== |
Equal | 相等 | 3 == 3 |
True |
!= |
Not Equal | 不等 | 3 != 4 |
True |
> |
Greater than | 大于 | 5 > 3 |
True |
< |
Less than | 小于 | 2 < 1 |
False |
>= |
Greater or equal | 大于等于 | 3 >= 3 |
True |
<= |
Less or equal | 小于等于 | 4 <= 3 |
False |
3.2 = 与 == 的严格区别¶
| 符号 | 类型 | 含义 |
|---|---|---|
= |
赋值运算符 | 将右侧的值赋给左侧变量 |
== |
比较运算符 | 判断两侧的值是否相等 |
# 赋值
x = 5 # 将 5 赋给 x
# 比较
print(x == 5) # True:x 的值等于 5 吗?
print(x == 6) # False
# 典型错误(在 Python 中会直接报语法错误)
if x = 5: # SyntaxError: invalid syntax
pass
# 正确写法
if x == 5:
print("x 等于 5")
竞赛场景:条件判断(
if/while)、排序自定义比较函数(key=lambda)、二分查找终止条件。
3.3 Python 特有:链式比较¶
Python 支持将多个比较运算符链式组合,语义与数学中完全一致:
x = 5
print(1 < x < 10) # True:相当于 (1 < x) and (x < 10)
print(1 <= x <= 5) # True
print(0 < x < 3) # False
# 等价但更繁琐的写法
print(1 < x and x < 10) # True(效果相同)
竞赛场景:区间合法性验证,如判断坐标是否在网格内:
0 <= r < rows and 0 <= c < cols。
3.4 记忆口诀¶
"单等号赋值,双等号比较;链式比较像数学,左右夹逼最自然。"
四、逻辑运算符¶
逻辑运算符用于组合多个布尔表达式,在条件判断和过滤中大量使用。
4.1 运算符列表¶
| 运算符 | 英文名称 | 含义 | 示例 | 结果 |
|---|---|---|---|---|
and |
Logical AND | 逻辑与(全真才真) | True and False |
False |
or |
Logical OR | 逻辑或(有真即真) | True or False |
True |
not |
Logical NOT | 逻辑非(取反) | not True |
False |
4.2 短路求值(Short-circuit Evaluation)¶
Python 的 and 和 or 具有短路特性:
A and B:若A为假,直接返回A,不计算BA or B:若A为真,直接返回A,不计算B
def check(x):
print(f"check({x}) called")
return x > 0
# and 短路:第一个条件为假时,第二个不执行
result = check(-1) and check(5)
# 仅输出:check(-1) called
# result = False
# or 短路:第一个条件为真时,第二个不执行
result = check(1) or check(5)
# 仅输出:check(1) called
# result = True
竞赛场景:边界条件保护。例如访问数组前先检查下标合法性:
if 0 <= i < n and arr[i] > 0。若i越界,短路机制保证arr[i]不被执行,避免IndexError。
4.3 综合示例:区间判断与网格 BFS¶
rows, cols = 5, 5
visited = [[False] * cols for _ in range(rows)]
def is_valid(r, c):
# 判断坐标 (r, c) 是否在网格内且未访问
return 0 <= r < rows and 0 <= c < cols and not visited[r][c]
# 四方向移动
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
r, c = 2, 2
for dr, dc in directions:
nr, nc = r + dr, c + dc
if is_valid(nr, nc):
print(f"可以移动到 ({nr}, {nc})")
4.4 记忆口诀¶
"
and全真才能真,or有真就为真,not一翻天地变;短路求值省时间。"
五、位运算符¶
位运算是算法竞赛的重要工具,直接操作整数的二进制表示,速度极快。掌握位运算是进阶竞赛的必要条件。
5.1 二进制基础¶
每个整数在计算机内部以二进制形式存储。例如:
十进制 → 二进制(8位)
5 → 00000101
6 → 00000110
12 → 00001100
位运算就是对这些二进制位逐位进行操作。
5.2 六种位运算符详解¶
& 按位与(Bitwise AND)¶
规则:对应位全为 1 才得 1,否则得 0。
5 = 0101
6 = 0110
-------
5&6 = 0100 = 4
print(5 & 6) # 4
print(5 & 1) # 1 ← 判断奇偶的核心操作
print(4 & 1) # 0
核心应用:判断奇偶
n & 1、取出某一位(n >> k) & 1、清零某一位。
| 按位或(Bitwise OR)¶
规则:对应位至少一个为 1 就得 1,全为 0 才得 0。
5 = 0101
6 = 0110
-------
5|6 = 0111 = 7
print(5 | 6) # 7
核心应用:将某一位强制置为 1(
n | (1 << k)将第 k 位置 1)。
^ 按位异或(Bitwise XOR)¶
规则:对应位不同得 1,相同得 0。
5 = 0101
6 = 0110
-------
5^6 = 0011 = 3
print(5 ^ 6) # 3
print(5 ^ 5) # 0 ← 任何数与自身异或得 0(自我消除)
print(0 ^ 5) # 5 ← 任何数与 0 异或得自身
重要性质:
a ^ a = 0(自我消除)a ^ 0 = a(与 0 异或不变)- 异或满足交换律与结合律
# 经典竞赛题:找出数组中只出现一次的数(其他数出现两次)
arr = [1, 2, 3, 2, 1]
result = 0
for x in arr:
result ^= x
print(result) # 3 ← 1^2^3^2^1 = 3
核心应用:查找单一元素、交换两数(无需临时变量,但可读性差,竞赛中了解即可)、状态压缩翻转位。
~ 按位取反(Bitwise NOT)¶
规则:每一位取反(0 变 1,1 变 0)。Python 中 ~n = -(n+1)。
print(~5) # -6 ← ~n = -(n+1)
print(~0) # -1
print(~(-1)) # 0
竞赛场景:
~i常用于某些位掩码技巧,但初学者在 Python 中需注意其结果恒为负数(Python 整数是无限精度有符号数)。
<< 左移(Left Shift)¶
规则:将所有位向左移动 k 位,右侧补 0。等价于乘以 $2^k$。
5 = 0000 0101
5 << 1 = 0000 1010 = 10 (× 2)
5 << 2 = 0001 0100 = 20 (× 4)
print(5 << 1) # 10 ← 5 × 2
print(5 << 3) # 40 ← 5 × 8
print(1 << 10) # 1024 ← 2^10,竞赛常用
核心应用:生成 2 的幂次
1 << k;状态压缩 DP 中表示第 k 个元素是否被选mask | (1 << k)。
>> 右移(Right Shift)¶
规则:将所有位向右移动 k 位,左侧补符号位。等价于整除 $2^k$(向下取整)。
20 = 0001 0100
20 >> 1 = 0000 1010 = 10 (÷ 2)
20 >> 2 = 0000 0101 = 5 (÷ 4)
print(20 >> 1) # 10 ← 20 // 2
print(20 >> 2) # 5 ← 20 // 4
print(7 >> 1) # 3 ← 7 // 2(向下取整)
核心应用:二分法中快速求中点
mid = (lo + hi) >> 1(效果等同于// 2,但更接近底层习惯);提取第 k 位(n >> k) & 1。
5.3 核心技巧:n & 1 判断奇偶¶
为什么 n & 1 可以判断奇偶?
任意整数的二进制表示中,最低位(第 0 位)决定奇偶性:
- 偶数最低位为
0(因为偶数是 2 的倍数,即 $\ldots \times 2 + 0$) - 奇数最低位为
1(因为奇数除以 2 余 1,即 $\ldots \times 2 + 1$)
n & 1 用掩码 000...001 与 n 按位与,结果就是 n 的最低位:
n = 6 = ...0110
1 = ...0001
-----------
6&1= ...0000 = 0 → 偶数
n = 7 = ...0111
1 = ...0001
-----------
7&1= ...0001 = 1 → 奇数
for n in [0, 1, 6, 7, 100, 101]:
parity = "奇数" if n & 1 else "偶数"
print(f"{n}: {parity}")
对比
n % 2:两者效果完全等价,n & 1在底层更直接,但在 Python 中性能差异可忽略。竞赛中两种写法均可接受。
5.4 综合示例:状态压缩(State Compression)¶
用一个整数的若干二进制位表示一个集合的选取状态,是竞赛 DP 的常见技巧:
# 用 mask 表示 4 个元素 [A, B, C, D] 的选取情况
# mask 的第 i 位为 1 表示第 i 个元素被选中
mask = 0b1010 # 十进制 10,表示选了 B(位1)和 D(位3)
# 检查第 i 个元素是否被选中
def is_selected(mask, i):
return (mask >> i) & 1 == 1
# 选中第 i 个元素
def select(mask, i):
return mask | (1 << i)
# 取消选中第 i 个元素
def deselect(mask, i):
return mask & ~(1 << i)
print(is_selected(mask, 1)) # True(第 1 位为 1)
print(is_selected(mask, 0)) # False(第 0 位为 0)
print(bin(select(mask, 0))) # 0b1011(增加 A)
5.5 记忆口诀¶
"与 AND 全 1 得 1,或 OR 有 1 得 1,异或 XOR 不同得 1;
左移乘 2 的幂,右移整除 2 的幂;
n & 1看末位,末位 1 奇 0 偶。"
六、竞赛常用数学函数¶
6.1 内置函数(无需导入)¶
abs(x) — 绝对值¶
print(abs(-7)) # 7
print(abs(3.14)) # 3.14
竞赛场景:曼哈顿距离
abs(x1-x2) + abs(y1-y2);误差判断abs(a - b) < 1e-9。
pow(a, b) 与 pow(a, b, mod) — 幂运算与快速幂¶
print(pow(2, 10)) # 1024(等同于 2**10)
print(pow(2, 10, 1000000007)) # 1024(快速幂取模)
print(pow(2, -1, 7)) # 4(模逆元,Python 3.8+)
pow(a, b, mod) 是竞赛必用函数,原因如下:
| 写法 | 时间复杂度 | 中间值大小 | 推荐度 |
|---|---|---|---|
a**b % mod |
$O(b)$(Python 内部优化) | 极大 | ❌ 不推荐 |
pow(a, b, mod) |
$O(\log b)$ | 始终 $< mod^2$ | ✅ 强烈推荐 |
round(x, n) — 四舍五入¶
print(round(3.567, 2)) # 3.57
print(round(2.5)) # 2(Python 3 使用银行家舍入)
竞赛场景:浮点精度问题处理(但竞赛中一般避免浮点,转为整数运算)。
6.2 math 模块函数(需要 import math)¶
import math
math.sqrt(x) — 平方根¶
print(math.sqrt(16)) # 4.0(返回浮点数)
print(int(math.sqrt(16))) # 4
# 竞赛中判断完全平方数
def is_perfect_square(n):
s = int(math.sqrt(n))
return s * s == n # 注意浮点误差,用整数验证
print(is_perfect_square(25)) # True
print(is_perfect_square(26)) # False
注意:
math.sqrt()返回浮点数,存在精度风险。判断完全平方数时务必用整数回验。
math.floor(x) 与 math.ceil(x) — 向下/向上取整¶
print(math.floor(3.9)) # 3(向下取整,等同于 //)
print(math.ceil(3.1)) # 4(向上取整)
print(math.floor(-3.1)) # -4(向负无穷)
print(math.ceil(-3.9)) # -3(向正无穷)
# 向上取整等价形式(整数场景,避免浮点)
# ceil(a / b) = (a + b - 1) // b
print((7 + 3 - 1) // 3) # 3,等同于 math.ceil(7/3)
竞赛技巧:
⌈a / b⌉ = (a + b - 1) // b是整数向上取整的标准替代方案,避免引入浮点数。
math.gcd(a, b) — 最大公约数¶
print(math.gcd(12, 8)) # 4
print(math.gcd(100, 75)) # 25
# 最小公倍数(LCM)= a * b / gcd(a, b)
def lcm(a, b):
return a // math.gcd(a, b) * b # 先除后乘,避免溢出
print(lcm(4, 6)) # 12
Python 3.9+:
math.gcd()支持多参数math.gcd(a, b, c);math.lcm(a, b)也已内置。
math.log(x, base) — 对数¶
print(math.log(8, 2)) # 3.0 ← log₂(8) = 3
print(math.log(1000, 10)) # 3.0 ← log₁₀(1000) = 3
print(math.log2(8)) # 3.0(精度更高)
print(math.log10(1000)) # 3.0(精度更高)
竞赛场景:估算算法复杂度、树的高度;判断 n 是否为 2 的幂:
n > 0 and (n & (n-1)) == 0(位运算更精确)。
6.3 函数功能分类速查¶
| 函数 | 用途分类 | 竞赛典型场景 |
|---|---|---|
pow(a, b, mod) |
防溢出 + 优化 | 快速幂、模逆元 |
math.gcd() |
数学优化 | 最大公约数、最小公倍数 |
abs() |
常规计算 | 距离、误差 |
math.sqrt() |
几何计算 | 欧氏距离(注意精度) |
math.floor() / math.ceil() |
取整 | 分组、上界估算 |
math.log2() |
算法分析 | 树高、二进制位数 |
6.4 记忆口诀¶
"快速幂用
pow(a,b,mod),最大公约数用gcd;开根号小心浮点陷阱,向上取整用(a+b-1)//b。"
七、运算符优先级¶
7.1 优先级表(从高到低)¶
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1(最高) | ** |
幂运算(右结合) |
| 2 | +x, -x, ~x |
一元正、负、按位取反 |
| 3 | *, /, //, % |
乘、除、整除、取余 |
| 4 | +, - |
加、减 |
| 5 | <<, >> |
位移运算 |
| 6 | & |
按位与 |
| 7 | ^ |
按位异或 |
| 8 | \| |
按位或 |
| 9 | ==, !=, >, <, >=, <= |
比较运算 |
| 10 | not |
逻辑非 |
| 11 | and |
逻辑与 |
| 12(最低) | or |
逻辑或 |
7.2 经典示例:3 + 2 * 4 为什么不是 20?¶
print(3 + 2 * 4) # 11,不是 20
原因:* 的优先级高于 +,因此先计算 2 * 4 = 8,再计算 3 + 8 = 11。若要强制先加后乘,需使用括号:
print((3 + 2) * 4) # 20
7.3 常见优先级陷阱¶
**陷阱 1:** 是右结合的**
print(2 ** 3 ** 2) # 512,不是 64
# 解析为 2 ** (3 ** 2) = 2 ** 9 = 512
print((2 ** 3) ** 2) # 64
陷阱 2:位运算优先级低于比较运算符(常见错误)
# 错误:以为先计算 n & 1 == 1
if n & 1 == 1: # 实际解析为 n & (1 == 1) = n & True = n & 1(恰好正确,但理解有误)
pass
# 更清晰的写法(竞赛推荐):
if (n & 1) == 1: # 加括号,意图明确
pass
竞赛建议:凡是混合使用位运算与比较运算的表达式,务必加括号,消除歧义。
陷阱 3:not 的优先级高于 and/or
print(not True or False) # False,解析为 (not True) or False = False or False
print(not (True or False)) # False,但含义完全不同
陷阱 4:% 与 +/- 的组合
print(-7 % 3) # 2(Python:结果符号与除数相同)
print(7 % -3) # -2(结果符号与除数相同)
# 与 C++ 不同,竞赛跨语言移植需注意
7.4 记忆口诀¶
"幂最高,乘除余,加减次,移位随;
位与 XOR 或,比较在后面;
not先and后or最末;
混用加括号,清晰无隐患。"
附录:竞赛场景速查表¶
| 竞赛场景 | 推荐写法 | 说明 |
|---|---|---|
| 快速幂取模 | pow(a, b, MOD) |
内置,$O(\log b)$ |
| 判断奇偶 | n & 1 或 n % 2 |
等价,前者更底层 |
| 循环数组下标 | (i + 1) % n |
自动回绕 |
| 大数取模防溢出 | result = result * x % MOD |
每步取模 |
| 字符串哈希 | h = (h * BASE + ord(c)) % MOD |
多项式滚动哈希 |
| 二分中点 | mid = (lo + hi) // 2 |
避免浮点,防止 C++ 溢出惯例 lo + (hi-lo)//2 |
| 向上取整(整数) | (a + b - 1) // b |
等价于 $\lceil a/b \rceil$,无浮点 |
| 最大公约数 | math.gcd(a, b) |
内置,高效 |
| 选中集合中第 k 位 | (mask >> k) & 1 |
状态压缩 DP |
| 将第 k 位置 1 | mask \| (1 << k) |
状态压缩 DP |
| 判断 2 的幂 | n > 0 and (n & (n-1)) == 0 |
位运算技巧 |
学习建议:建议在 OJ 上针对每个运算符分别练习 2-3 道相关题目,重点掌握取模(第二节)和位运算(第五节)的实际应用,它们是竞赛进阶题的高频考点。
Python 函数参数机制¶
一、引言:为什么需要深入理解参数机制?¶
当你阅读 PyTorch 的函数签名时,经常会看到这样的写法:
def zeros(*size, *, out=None, dtype=None, device=None, requires_grad=False) -> Tensor
初学者往往只关注"传什么值",而忽略"为什么可以这样传"。事实上,这一行签名背后涉及 Python 函数参数体系的四个核心概念:普通位置参数、可变位置参数、强制关键字参数分隔符、以及带默认值的关键字参数。理解这套体系,不仅能读懂 PyTorch 的 API 文档,更能写出更清晰、更健壮的 Python 代码。
二、Python 参数的完整分类¶
Python 的函数参数按照传递方式可以分为五类,彼此之间有严格的顺序约束。我们先建立全局视图,再逐一深入。
def func(a, b, *args, keyword_only, **kwargs):
pass
在这个签名中,a 和 b 是普通位置参数,*args 是可变位置参数,keyword_only 是强制关键字参数,**kwargs 是可变关键字参数。Python 规定,函数调用时传入的参数必须按照这个顺序匹配,任何违反顺序的传参都会在运行时抛出 TypeError。
三、普通位置参数(Positional Arguments)¶
这是最基础的参数形式。调用时按照参数定义的顺序,从左到右依次匹配。
def add(x, y):
return x + y
add(3, 5) # x=3, y=5 ✅
add(5, 3) # x=5, y=3 ✅(顺序不同,结果不同)
add(x=3, y=5) # 也合法,以关键字方式传入位置参数
普通位置参数可以按位置传,也可以按关键字传,两者均合法。这为后续理解"强制关键字参数"的设计动机奠定了基础——有时候,我们明确不希望某些参数被按位置传入。
四、可变位置参数 *args:打包任意数量的位置参数¶
在参数名前加单个 *,表示这个位置可以接受零个或任意多个位置参数,Python 会将它们统一打包为一个元组(tuple)传入函数体。
def show(*args):
print(type(args), args)
show(1, 2, 3) # <class 'tuple'> (1, 2, 3)
show("a", "b") # <class 'tuple'> ('a', 'b')
show() # <class 'tuple'> ()
这正是 torch.zeros(*size, ...) 能够接受以下三种调用方式的根本原因:
torch.zeros(3) # size = (3,)
torch.zeros(2, 3) # size = (2, 3)
torch.zeros(2, 3, 4) # size = (2, 3, 4)
*args 与传入列表/元组的区别。 一个常见的混淆点是:torch.zeros(2, 3) 和 torch.zeros([2, 3]) 的行为看起来相同,但底层机制不同。前者是"传入两个独立整数,*size 将它们打包为 (2, 3)";后者是"传入一个列表对象,*size 将其打包为 ([2, 3],),即一个只含一个元素的元组"。PyTorch 在函数内部检测到后者的情况时,会自动将列表拆开解读,这是框架层面的兼容性处理,而非 Python 语法本身的行为。
理解了这一点,你就能理解 * 拆包操作符的价值:
shape = [2, 3]
torch.zeros(*shape) # 等价于 torch.zeros(2, 3),手动将列表拆开传入
五、强制关键字参数分隔符 *(Keyword-Only Arguments)¶
这是 Python 3 引入的一个语法,也是最容易被忽视的机制之一。在函数签名中,单独出现的 *(不跟参数名)起到分隔符的作用:它左侧的参数可以按位置传入,它右侧的所有参数必须以关键字形式传入,否则直接报错。
def func(a, b, *, c, d=10):
print(a, b, c, d)
func(1, 2, c=3) # ✅ a=1, b=2, c=3, d=10
func(1, 2, c=3, d=4) # ✅ a=1, b=2, c=3, d=4
func(1, 2, 3) # ❌ TypeError: func() takes 2 positional arguments but 3 were given
回到 PyTorch 的签名 torch.zeros(*size, *, out=None, dtype=None, ...),这里同时出现了两个 *,含义截然不同:第一个 *size 是"吸收所有位置参数",第二个单独的 * 是"分隔符,其后所有参数必须用关键字传"。设计意图非常清晰:shape 相关的整数可以简洁地按位置传入,而控制行为的配置项(dtype、device 等)必须显式命名,防止位置错乱导致的隐蔽 Bug。
# 如果没有 * 分隔符,下面这行调用会产生严重歧义
torch.zeros(2, 3, some_tensor) # some_tensor 是 size 的一部分?还是 out?
# 有了 * 分隔符,强制要求写清楚
torch.zeros(2, 3, out=some_tensor) # 无歧义 ✅
六、带默认值的参数(Default Arguments)¶
参数可以携带默认值,调用时若不传入该参数,则使用默认值。默认值参数必须排在无默认值参数的后面,这是 Python 的语法强制要求。
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}")
greet("Wei Jin") # Hello, Wei Jin
greet("Wei Jin", "Hi") # Hi, Wei Jin
一个经典的陷阱:默认值是可变对象。 当默认值是列表、字典等可变对象时,该对象在函数定义时只被创建一次,而非每次调用时重新创建。这会导致跨调用的状态污染:
# 危险写法
def append_to(element, to=[]):
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← 不是 [2]!默认列表被复用了
# 正确写法
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
PyTorch 中大量使用 None 作为默认值并在函数体内判断,正是遵循了这一最佳实践。
七、可变关键字参数 **kwargs¶
在参数名前加 **,表示接受任意数量的关键字参数,Python 将它们打包为一个字典(dict)传入。
def show(**kwargs):
print(type(kwargs), kwargs)
show(a=1, b=2) # <class 'dict'> {'a': 1, 'b': 2}
**kwargs 常用于需要透传参数的场景,例如封装函数:
def my_zeros(*size, **kwargs):
print(f"创建形状为 {size} 的张量,配置项:{kwargs}")
return torch.zeros(*size, **kwargs)
my_zeros(2, 3, dtype=torch.float64, device='cpu')
八、完整参数顺序规则¶
Python 规定,函数签名中各类参数必须严格遵循以下顺序,否则 SyntaxError:
普通位置参数 → *args → 强制关键字参数 → **kwargs
(a, b) (*args) (c, d=10) (**kwargs)
对应到 PyTorch 的真实签名,可以完整地对号入座:
# 普通位置参数 *args *分隔符 强制关键字参数(含默认值)
def zeros(*size, *, out=None, dtype=None, layout=..., device=None, requires_grad=False, pin_memory=False):
这里没有普通位置参数,*size 直接承担了所有位置参数的接收任务;单独的 * 之后,所有参数都是强制关键字参数。
九、小结¶
本专题覆盖的五类参数机制及其核心要点如下。
普通位置参数是最基础的形式,可以按位置或关键字传入,两者均合法。可变位置参数 *args 将任意数量的位置参数打包为元组,是 torch.zeros(2, 3) 这类 API 能够灵活接受任意维度 shape 的语法基础。强制关键字参数分隔符 * 通过划定"位置区"与"关键字区",消除了参数传递的歧义性,也是 PyTorch API 设计的核心惯例之一。带默认值的参数提供了调用时的灵活性,但需警惕可变对象作为默认值的陷阱。**可变关键字参数 **kwargs** 则将任意关键字参数打包为字典,常用于参数透传与封装场景。
掌握这套体系之后,PyTorch 乃至整个 Python 生态中绝大多数 API 的函数签名都将变得可读、可预测。