Python 语法
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__ 的标准写法。