Day 01|Python 基础速通:数据结构、函数与 NumPy 入门(AI 学习笔记 · 基础筑基周 · 第 1 篇)

30 天 AI 学习计划 Day 1:Python 基础语法速通,为后续机器学习打地基。

Day 01|Python 基础速通:数据结构、函数与 NumPy 入门

Python 是 AI 工程的事实标准语言,但会写 if/for/print 只算「识字」;真正进入 AI 工程需要把 数据结构选择、函数封装、NumPy 向量化这三件事内化成肌肉记忆——它们决定了后续 30 天写代码是「调包一行跑」还是「for 循环卡到天黑」。


1. 内置数据结构:四把武器

1.1 list(列表):可变有序序列

xs = [3, 1, 4, 1, 5, 9, 2, 6]
xs.append(7)          # 末尾追加,O(1)
xs.insert(0, 99)      # 头部插入,O(n)
xs[2:5]               # 切片,左闭右开
[x * x for x in xs if x > 3]   # 列表推导式,常用
操作 时间复杂度 备注
append O(1) 均摊 末尾追加极快
insert(0, x) O(n) 头部插入要搬所有元素
x in xs O(n) 成员检测要扫描
xs[i] O(1) 按下标读

AI 中的角色:存放批量样本、做 mini-batch 切片、记录训练日志。坑:循环里反复 append 大对象比预分配 list 慢,因为 CPython 解释器每条语句都有调度开销。

1.2 dict(字典):哈希表映射

word_to_idx = {"<pad>": 0, "<unk>": 1, "the": 2, "cat": 3}
idx = word_to_idx.get("dog", word_to_idx["<unk>"])  # 安全查,缺时回退
操作 时间复杂度
d[k] / d[k] = v O(1) 均摊
k in d O(1)
遍历 items O(n)

AI 中的角色:词表、类别索引、配置项、模型 state_dict 全部用 dict 表达。坑:用可变对象(list / dict / set)做 key 会立刻抛 TypeError: unhashable type,只能拿不可变对象(str / int / tuple)做 key。

1.3 set(集合):去重 + 集合运算

A = {1, 2, 3, 4}; B = {3, 4, 5, 6}
A & B   # 交集 → {3, 4}
A | B   # 并集 → {1, 2, 3, 4, 5, 6}
A - B   # 差集 → {1, 2}

AI 中的角色:训练集/验证集/测试集 ID 去重、做交叉验证的 fold 划分、过滤停用词。坑:空集合必须写 set(),{} 是空字典。

1.4 tuple(元组):不可变序列

pt = (3, 4)
x, y = pt              # 解包
d = {(0, 0): "origin", (1, 0): "x+"}   # 可作 dict key

AI 中的角色:函数返回多值、shape 描述(如 (B, C, H, W))、numpy 数组的轴交换索引。坑:单元素 tuple 必须写成 (x,) 而不是 (x),后者只是加括号的表达式。

1.5 四者对比表

容器 可变 有序 元素可重复 查找复杂度 典型 AI 用途
list 是 是 是 O(n) batch、序列、日志
dict 是 插入序(Python 3.7+) key 唯一 O(1) 词表、配置、state_dict
set 是 否 否 O(1) 去重、集合运算
tuple 否 是 是 O(n) 形状描述、复合 key

1.6 容器嵌套:数据结构的真实形态

dataset = [
    {"id": 0, "tokens": [2, 15, 88, 3], "label": 1},
    {"id": 1, "tokens": [2, 71, 4],     "label": 0},
]
vocab = {"<pad>": 0, "<unk>": 1, "the": 2}

AI 中的典型结构:外层 list 装样本,每个样本是 dict,dict 里有「token id 序列」这种嵌套 list。这种「list of dict」的形态是 PyTorch DataLoader 处理数据时的事实标准。

1.7 collections 模块:进阶武器

数据结构 来源 优势 AI 用途
deque collections.deque 两端 O(1) 增删 滑动窗口特征
Counter collections.Counter 频次统计 类别分布、词频
OrderedDict collections.OrderedDict 显式有序 Python 3.7+ 后 dict 已自带顺序
defaultdict collections.defaultdict 缺 key 自动建默认值 词表构建、邻接表
from collections import Counter, defaultdict
cnt = Counter(["cat", "dog", "cat", "fish"])  # Counter({'cat': 2, 'dog': 1, 'fish': 1})

vocab = defaultdict(int)
for tok in tokens:
    vocab[tok] += 1     # 缺 key 时自动创建 0 再 +=1

2. 函数:从 def 到纯函数

2.1 函数定义三要素

def predict(x, w, b):
    """线性预测:y = w·x + b"""
    return w * x + b

2.2 *args / **kwargs:可变参数

def log_metrics(metrics, *tags, **opts):
    for k, v in metrics.items():
        line = f"{k}={v:.4f}"
        if opts.get("bold"):
            line = "**" + line + "**"
        print(" ".join([line, *tags]))

*args 收集成 tuple,**kwargs 收集成 dict;用于「我不知道调用方会传几个参数」的库函数。

2.3 默认参数陷阱

# 错误写法:lst 在函数定义时只创建一次
def append_bad(x, lst=[]):
    lst.append(x)
    return lst

print(append_bad(1))   # [1]
print(append_bad(2))   # [1, 2] ← 状态污染!
# 正确写法:每次显式创建
def append_good(x, lst=None):
    if lst is None:
        lst = []
    lst.append(x)
    return lst

根因:Python 函数定义时就把默认实参对象求值并绑定,后续所有调用共享同一对象。AI 中常见坑:def train(model, optimizer=Adam(model.parameters())) 会让所有 train 调用共用同一个 optimizer,训练一会就乱套。

2.4 lambda 与高阶函数

sorted(scores, key=lambda kv: -kv[1])[:10]
list(map(lambda x: x ** 2, range(10)))
list(filter(lambda x: x % 2 == 0, range(10)))

lambda 只适合写一行能写完的简单函数,复杂逻辑请用 def。


3. NumPy 基础:ndarray

3.1 为什么必须学 NumPy

1000×1000 矩阵相乘:

方式 耗时
纯 Python for 循环 约 12 秒
NumPy @ 约 30 毫秒

快约 400 倍,因为 NumPy 底层是 C 实现 + BLAS + SIMD 向量化指令。

3.2 ndarray 属性

import numpy as np
a = np.arange(12).reshape(3, 4)
print(a.shape)        # (3, 4)
print(a.dtype)        # int64
print(a.ndim)         # 2
print(a.size)         # 12
print(a.strides)      # (32, 8) — 每跨一行/列要跳多少字节

3.3 创建数组 6 种方式

np.zeros((3, 4))            # 全 0
np.ones((3, 4), dtype=np.float32)   # 全 1
np.full((3, 4), 7.0)       # 全 7
np.eye(4)                   # 单位矩阵
np.arange(0, 10, 0.5)      # 等差,类似 range
np.linspace(0, 1, 5)        # 5 个点把 [0,1] 均分
np.random.randn(3, 4)       # 标准正态

3.4 索引与切片

a = np.arange(12).reshape(3, 4)
a[1, 2]            # 元素:7
a[1]               # 第 2 行:array([4,5,6,7])
a[:, 1]            # 第 2 列:array([1,5,9])
a[a > 5]           # 布尔索引:array([6,7,8,9,10,11])
a[[0, 2], [1, 3]]  # 花式索引:(0,1) 和 (2,3) 两个元素

坑:a[:, 0] 返回的是视图(和原数组共享内存),修改它会改原数组;a[:, 0:1] 返回的是副本。


4. NumPy 向量化运算

4.1 element-wise 与矩阵乘

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

a + b        # element-wise 加
a * b        # element-wise 乘,不是矩阵乘!
a @ b        # 真正的矩阵乘:[[19,22],[43,50]]
np.dot(a, b) # 等价于 a @ b

核心区别:* 是「对应位置相乘」;@ 是线性代数定义的矩阵乘。混淆这两者是初学者最常见的错。

4.2 三种「乘」的区别

操作 符号 1D 2D ND
element-wise * 对应位相乘 对应位相乘 对应位相乘
内积 / 矩阵乘 np.dot / @ 内积(sum) 矩阵乘 最后两维做矩阵乘,前面当 batch
np.matmul @ 内积 矩阵乘 batch 维广播,与 dot 在 1D 上有细微差异

4.3 聚合与 axis

a = np.arange(24).reshape(2, 3, 4)
a.sum()              # 全部求和:276
a.sum(axis=0)        # 沿第 0 轴压扁:(3,4)
a.sum(axis=1)        # 沿第 1 轴压扁:(2,4)
a.sum(axis=(0, 2))   # 沿 0 和 2 同时压扁:(3,)

axis=k 表示「沿第 k 维做归约,该维消失」。keepdims=True 可保留被压扁的维为 1,方便广播。

4.4 范数与距离

x = np.array([3.0, 4.0])
np.linalg.norm(x)            # L2 范数:5.0
np.linalg.norm(x, ord=1)     # L1 范数:7.0
np.linalg.norm(x, ord=np.inf)  # L∞ 范数:4.0

4.5 实战对比:三种方式算 sum-of-squares

import numpy as np
import time

x = np.random.randn(10_000_000)

# 方式 1:Python sum + 生成器
t0 = time.time()
s1 = sum(v * v for v in x)
t1 = time.time(); print(t1 - t0, "s")   # 约 2.5 秒

# 方式 2:numpy sum
t0 = time.time()
s2 = (x ** 2).sum()
t1 = time.time(); print(t1 - t0, "s")   # 约 0.02 秒

# 方式 3:numpy dot 内积(等价)
t0 = time.time()
s3 = x @ x
t1 = time.time(); print(t1 - t0, "s")   # 约 0.005 秒

数量级差异:Python ≈ 2.5 s / numpy.sum ≈ 20 ms / numpy.dot ≈ 5 ms。x @ x 还快于 (x**2).sum() 是因为 **2 会显式分配一个新数组,@ 走 BLAS 的点积内核直接算。AI 实战准则:能用 BLAS 内核(dot / matmul / einsum)就别用 element-wise 三段式。

4.6 einsum:爱因斯坦求和约定

# 矩阵乘 C[i,j] = Σ_k A[i,k] * B[k,j]
C = np.einsum("ik,kj->ij", A, B)

# batch 矩阵乘 (B,N,M) @ (B,M,P) → (B,N,P)
out = np.einsum("bnm,bmp->bnp", X, W)

# 沿 axis 求和:等价于 X.sum(axis=k)
np.einsum("ij->i", X)

einsum 把「对哪些维求和、对哪些维保留」写在字符串里,永远不需要临时 reshape;复杂张量运算首选。


5. 广播机制(Broadcasting)

5.1 规则(从右往左对齐)

  1. 如果两个数组维度数不同,小维度的数组左侧补 1
  2. 沿每个维度,如果两者大小相同或其中一个为 1,则可广播
  3. 维度为 1 的那一维会被「拉伸」到与另一维相同,不复制数据
A = np.arange(6).reshape(2, 3)   # (2,3)
b = np.array([10, 20, 30])       # (3,) → (1,3)
C = A + b                          # (2,3) + (1,3) → (2,3)

5.2 5 个典型例子

A shape B shape A+B 结果 shape 说明
(3, 1) (1, 4) (3, 4) 行向量 × 列向量 = 矩阵
(4, 1, 3) (2, 1) (4, 2, 3) B 补 1 维 → (1,2,1) 再广播
(8, 4) (4,) (8, 4) b 补 1 维 → (1,4) 再广播
(3, 4) (3, 1) (3, 4) 列方向广播
(3, 4) (3,) error 不能广播(最后对齐后 4 vs 3)

5.3 神经网络里到处都是广播

# (B, C) 的特征减去均值,沿 feature 维
mean = x.mean(axis=0, keepdims=True)   # (1, C)
x_centered = x - mean                  # (B, C) - (1, C) → (B, C)

6. PyTorch / NumPy 互操作(衔接 Day 16)

import numpy as np
import torch

a = np.arange(6, dtype=np.float32).reshape(2, 3)
t = torch.from_numpy(a)       # 共享内存!
t2 = torch.tensor(a)          # 复制
b = t.numpy()                 # 转回去,共享内存

共享内存意味着改 NumPy 会影响 Tensor,反过来亦然。后续 PyTorch 章节会大量用到这个互转。

6.1 dtype 对齐

NumPy 默认 int64 / float64,PyTorch 默认 int64 / float32。模型输入若用 numpy 喂 double 进去,PyTorch 会立刻报 RuntimeError: mat1 and mat2 must have the same dtype,永远是 dtype 不匹配。修法:构造数据时显式 .astype(np.float32),或在 tensor 侧 .float()。

6.2 零拷贝 vs 复制

操作 内存 速度
torch.from_numpy(a) 共享 O(1)
torch.tensor(a) 复制 O(n)
tensor.numpy() 共享 O(1)
tensor.cpu().numpy() 共享 + 跨设备 涉及 CUDA 同步

坑:GPU 上的 tensor 不能直接 .numpy(),要先 .cpu();CUDA tensor 与 numpy 永远不共享内存,这是 CUDA 模型必须的事实。


7. 常见坑(7 条)

7.1 list 索引 vs numpy 索引混用

症状:TypeError: only integer scalar arrays can be converted to a scalar index 原因:把 numpy 数组当 list 用 a[[1, 2]] 是花式索引,行为不同于 a[1, 2](后者是二维索引) 修法:始终明确「我在用 list 还是 ndarray」,二者索引语义不同

7.2 默认参数持有可变对象

症状:函数多次调用结果「串味」 原因:默认参数对象在函数定义时创建,后续共享 修法:默认参数设为 None,函数内显式创建

7.3 * 与 @ 混淆

症状:a * b 想做矩阵乘,结果 shape 不对 原因:* 是 element-wise,@ 才是矩阵乘 修法:矩阵乘永远用 a @ b 或 np.matmul(a, b)

7.4 广播维度不对齐

症状:ValueError: operands could not be broadcast together with shapes (3,4) (3,) 原因:最后对齐时维度不匹配,且都不为 1 修法:b = b.reshape(1, -1) 或 b = b[:, None] 显式升维

7.5 np.dot 在 1D/2D 的行为差异

症状:np.dot(v1, v2) 返回标量,np.dot(M1, M2) 返回矩阵,写法相似但语义不同 原因:np.dot 对 1D 是内积、对 2D 是矩阵乘 修法:涉及 1D 向量用 v1 @ v2,涉及矩阵用 M1 @ M2,统一用 @

7.6 修改切片影响原数组

症状:a[0, :] = 999 把原数组某行改了,自己都没注意 原因:基础切片返回视图而非副本 修法:需要副本时显式 a[0, :].copy()

7.7 整数除法 vs 浮点除法

症状:3 / 2 == 1 让人懵 原因:Python 3 之前 / 是整除,Python 3 之后 / 是真除法但整数运算可能被类型推断成 int 修法:写 3.0 / 2 或 from __future__ import division;numpy 默认是浮点

7.8 append 迭代累积

症状:循环里反复 arr.append(x),比预分配慢 10 倍 原因:list 没预留容量,每次 append 满了就重新分配 修法:能预分配就用 numpy ndarray,Python 场景可先 [None] * N 再按下标填

7.9 深拷贝 vs 浅拷贝

症状:b = a; b[0] = 99 把 a 也改了 原因:赋值是引用绑定,b 和 a 指向同一个 list 修法:b = a.copy()(浅拷贝)或 b = copy.deepcopy(a)(深拷贝,递归复制嵌套对象)

7.10 np.random.seed 不够用

症状:设了 seed 结果还是每次不同 原因:np.random 默认全局状态,多进程/多线程下被覆盖 修法:用 np.random.default_rng() 创建独立 Generator 实例,或换 PyTorch 的 torch.Generator


8. 自检三问

A. 给你 100 万条 (user_id, score) 数据,要求按 user_id O(1) 查 score,选 list 还是 dict?为什么?如果还要按 score 排序输出 top 10,又该用什么数据结构配合?

要点:必须用 dict,键是 user_id,值是 score,因为 dict 基于哈希表查找是 O(1) 均摊,而 list 是 O(n)。Top 10 排序:从 dict 拿到 (user_id, score) 列表,用 heapq.nlargest(10, items, key=lambda kv: kv[1]) 维护大小为 10 的小顶堆,O(n log 10) ≈ O(n),比全排序 sorted(items, key=..., reverse=True)[:10] 在大 n 下省一个 log 因子。

B. 为什么 def f(x, lst=[]) 是反模式?请给出一种正确写法,并说出 AI 代码里你见过因为「可变默认参数」导致的状态污染 bug。

要点:Python 函数定义时就把默认实参对象求值,lst=[] 创建的空 list 被绑定到函数对象的 __defaults__ 上,所有调用共享同一对象。正确写法:def f(x, lst=None): lst = lst if lst is not None else []。AI 常见 bug:训练脚本里 def train(model, optimizer=Adam(model.parameters())) 会让所有 train 调用共用同一个 optimizer 状态。

C. 一句话解释 NumPy 广播机制,并写出 A 形状 (4, 1, 3) 与 B 形状 (2, 1) 相加后的结果 shape;再说明 A @ B 与 A * B 的本质区别。

要点:广播 = 维度为 1 时沿该轴「虚拟拉伸」对齐,从右往左对齐。A(4,1,3) + B(2,1):B 补 1 维成 (1,2,1),再对齐到 (4,2,3)。A @ B 是矩阵乘(线性代数),A * B 是 element-wise(对应位相乘)。Day 2 线性代数的矩阵乘就是 @,这是把神经网络「一层」从 for 循环改写成一行 X @ W + b 的根本。


9. 推荐资源

视频

教科书

论文

博客 / 课程

代码


10. 本节要点


11. 下一节:Day 02 · 线性代数核心

主题:向量、矩阵、点积、特征值。覆盖:

产出物:np.dot vs * 的 shape 推演表,PCA 在二维 toy 数据上的 1 行实现 + 可视化。

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。