学习内容: Python 基础语法、函数、数据结构、面向对象、异常处理、NumPy 数组计算、PyTorch Tensor。
学习目标: 从 C++ 过渡到 Python,掌握神经网络开发需要的编程基础,为后续 CNN 降噪模型训练做准备。
复习原则:
- 保留自己编写的练习代码。
- 记录实际出现的错误及原因。
- 提供修改后的正确代码。
- 详细整理每个已经讲过的知识点。
- 通过 Python 与 C++ 的对照理解语法差异。
第一章:Python 基础语法
1.1 Python 与 C++ 的主要区别
Python 是一种高级、动态类型编程语言。
与 C++ 相比,Python 的语法更加简洁,许多操作由语言运行时自动完成。
代码对比
C++:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
cout << a << endl;
return 0;
}
Python:
a = 10
print(a)
两段程序均输出 10。
主要差异
Python 的简洁并不意味着底层不存在复杂操作,而是很多机制被运行时和标准库封装起来。
1.2 变量与数据类型
Python 定义变量时不需要指定类型:
a = 10
b = 3.14
name = "Eidovelis"
flag = True
常见数据类型:
int整数10float浮点数3.14str字符串"Hello"bool布尔值True、Falselist列表[1, 2, 3]tuple元组(1, 2, 3)dict字典{"a": 10}set集合{1, 2, 3}type():获取数据类型
a = 10
print(type(a)) # <class 'int'>
a = "Hello"
print(type(a)) # <class 'str'>
Python 的变量名可以重新绑定到不同类型的对象。
例如:
a = 10
a = "Hello"
这在 Python 中是合法的。
但需要注意:动态类型不代表没有类型,只是不要求变量名始终绑定同一种类型。
1.3 Python 的运算符
算术运算符
3 + 2-减法3 - 2*乘法3 * 2/除法5 / 2 = 2.5//向下取整除法5 // 2 = 2%取模5 % 2 = 1**幂运算2 ** 3 = 8需要特别注意:
print(5 / 2) # 2.5
print(5 // 2) # 2
Python 的 / 通常得到浮点数结果。
而 // 表示向负无穷方向取整,不是简单地去掉小数部分。例如 -5 // 2 得到 -3。
比较运算符
a == b # 等于
a != b # 不等于
a > b # 大于
a < b # 小于
a >= b # 大于等于
a <= b # 小于等于
特别区分:
a = 10 # 赋值
a == 10 # 判断是否相等
复合赋值
a += 1
a -= 1
a *= 2
a /= 2
例如:
total += num
等价于:
total = total + num
Python 没有 C++ 那样的 i++、i-- 运算符。
第二章:条件判断、循环与函数
2.1 if、elif、else
Python 的条件判断结构:
a = 20
if a > 30:
print("大于30")
elif a > 10:
print("大于10")
else:
print("小于等于10")
C++ 与 Python 对照:
执行规则
if...elif...else 组成一个条件分支结构。
Python 从上往下判断:
- 如果
if成立,执行其代码块。 - 否则检查下一个
elif。 - 如果前面条件都不成立,执行
else。 - 一个分支执行后,不会再执行同一结构中的其他分支。
两个独立 if 与 elif 的区别
if a > 10:
print("A")
if a > 5:
print("B")
如果 a=20,会同时输出 A 和 B。
而:
if a > 10:
print("A")
elif a > 5:
print("B")
只会输出 A。
因此,在最大值和最小值统计中,可以使用两个独立的 if。
2.2 for 循环
Python 的 for 循环可以直接遍历可迭代对象。
numbers = [10, 20, 30]
for num in numbers:
print(num)
输出:
10
20
30
对应 C++ 范围 for:
for (int num : numbers)
{
cout << num << endl;
}
Python 的 for 与 C++ 的范围 for 在使用上较为相似。
range() 的详细用法
语法:
range(start, stop, step)
参数含义:
start:起始值,默认 0。stop:结束边界,不包含该值。step:步长,默认 1。
例如:
for i in range(5):
print(i)
依次输出:
0
1
2
3
4
再例如:
for i in range(1, 6):
print(i)
输出 1、2、3、4、5。
指定步长:
for i in range(0, 10, 2):
print(i)
输出:
0
2
4
6
8
反向遍历:
for i in range(5, 0, -1):
print(i)
输出:
5
4
3
2
1
注意:range() 返回的是 range 对象,不是直接创建包含所有整数的列表。
如果想转换成列表:
a = list(range(1, 6))
print(a)
结果:
[1, 2, 3, 4, 5]
2.3 while 循环
Python:
i = 0
while i < 5:
print(i)
i += 1
对应 C++:
int i = 0;
while (i < 5)
{
cout << i << endl;
i++;
}
两者的逻辑相同:
- 判断条件。
- 条件成立,执行循环体。
- 再次判断条件。
- 条件不成立,结束循环。
Python 同样支持:
break # 立即结束循环
continue # 跳过本次循环的剩余语句
2.4 Python 函数
函数定义语法:
def function_name(parameters):
# 函数体
return result
例如:
def add(a, b):
return a + b
result = add(10, 20)
print(result)
输出 30。
def 的作用
def 用来定义函数。
参数
def add(a, b):
a 和 b 是形参。
调用时:
add(10, 20)
10 和 20 是传入的实参。
return
return a + b
负责结束当前函数调用,并将计算结果返回给调用方。
需要区分:
print(a + b)
只是输出结果。
而:
return a + b
会将结果返回,以便后续保存或参与计算。
如果函数没有显式返回值,通常返回 None。
2.5 练习 01:偶数求和
题目: 定义函数,计算 1 到指定整数之间所有偶数的和。
我的原始代码
def evennumber_sum():
total = 0
for i in (1, n+1):
total = total + 2
return total
result = evennumber_sum(5)
print(result)
错误一:函数参数缺失
定义:
def evennumber_sum():
表示没有声明接收参数。
但调用:
evennumber_sum(5)
传入了一个参数。
应修改为:
def evennumber_sum(n):
错误二:误用元组
for i in (1, n+1):
这里的 (1, n+1) 是元组,并不是连续整数范围。
若 n=5,它只包含两个值:1 和 6。
应该使用:
for i in range(1, n+1):
错误三:累加逻辑错误
total = total + 2
只会在每次循环时让 total 增加 2。
但题目要求累加符合条件的整数本身。
修改后的代码
def evennumber_sum(n):
total = 0
for i in range(1, n + 1):
if i % 2 == 0:
total += i
return total
result = evennumber_sum(100)
print(result)
输出:
2550
相关练习:奇数判断
奇数除以 2 的余数为 1:
if i % 2 == 1:
因此,只需要修改条件就可以把偶数求和程序改为奇数求和程序。
本章复习重点
重点理解以下内容:
- Python 不使用
{}划分代码块。 :与缩进决定代码结构。range()的结束边界不包含在范围内。for可以遍历列表等可迭代对象。return和print()的区别。- 函数参数必须与调用方式对应。
if和elif的执行逻辑不同。
第三章:Python 列表与列表推导式
3.1 list 列表
列表是一种有序、可修改的数据容器。
numbers = [10, 20, 30, 40]
与 C++ 的 std::vector 在使用方式上有相似之处。
索引访问
print(numbers[0]) # 10
print(numbers[1]) # 20
索引从 0 开始。
负数索引
Python 允许使用负数索引:
numbers[-1] # 最后一个元素
numbers[-2] # 倒数第二个元素
例如:
numbers = [10, 20, 30, 40]
print(numbers[-1])
输出 40。
负数索引是 Python 列表非常常用的特性。
3.2 append() 与 len()
append()
在列表末尾添加一个元素。
numbers = [10, 20, 30]
numbers.append(40)
print(numbers)
输出:
[10, 20, 30, 40]
类似 C++:
numbers.push_back(40);
len()
获取列表元素数量:
len(numbers)
相当于 C++ 中的:
numbers.size();
例如:
numbers = [10, 20, 30]
print(len(numbers))
输出 3。
3.3 Python 的对象引用
这是学习时专门讨论过的一个概念。
Python 没有普通语法层面的 C++ 式指针声明。
C++:
int a = 10;
int* p = &a;
cout << *p;
Python 不能写:
a = 10
p = &a
但 Python 存在对象引用机制。
例如:
a = [10, 20, 30]
b = a
b[0] = 100
print(a)
结果:
[100, 20, 30]
原因是 a 和 b 引用同一个列表对象。
这里并不是复制了一份独立列表。
可变对象与不可变对象
列表是可变对象:
a = [10, 20, 30]
b = a
b[0] = 100
会改变双方引用的同一个列表。
整数是不可变对象:
a = 10
b = a
b = 20
print(a)
输出仍然是 10。
因为 b = 20 是重新绑定对象,并没有修改整数 10。
总结
Python 变量名通常绑定到对象。
C++ 则可以直接操作变量存储、指针与内存地址。
Python 中遍历列表通常不需要手动移动指针。
3.4 练习 02:寻找列表最大值
我的原始代码
numbers = [12, 45, 7, 89, 23, 56]
Maxvalue = 0
for num in numbers:
if Maxvalue < num:
Maxvalue = num
print(Maxvalue)
对于本题的正数数据,可以正确输出 89。
潜在错误
Maxvalue = 0
如果所有数字都是负数,例如:
numbers = [-12, -45, -7, -89]
程序会错误地输出 0。
修改后的代码
numbers = [12, 45, 7, 89, 23, 56]
max_value = numbers[0]
for num in numbers:
if num > max_value:
max_value = num
print(max_value)
本题核心
用列表第一个元素作为初始最大值,可以正确处理普通的非空数值列表,包括全负数情况。
3.5 练习 03:最大值、最小值和平均值
我的原始代码
numbers = [15, 28, 7, 42, 19, 36, 11]
Max_value = numbers[0]
Mini_value = numbers[0]
average = 0
sum = 0
for num in numbers,:
sum += num
if Max_value < num:
Max_value = num
if Mini_value > num:
Mini_value = num
average = sum / len(numbers)
print(Max_value)
print(Mini_value)
print(average)
错误一:for 后面多了逗号
for num in numbers,:
numbers, 在这里形成一个只包含列表的元组。
因此循环的 num 取得的是整个列表,而不是列表中的某个数字。
正确写法:
for num in numbers:
错误二:变量名与内置函数重名
sum = 0
Python 自带 sum() 函数。
虽然把变量命名为 sum 不一定立即报错,但会遮蔽当前作用域中的内置函数。
建议改成:
total = 0
修改后的代码
numbers = [15, 28, 7, 42, 19, 36, 11]
max_value = numbers[0]
min_value = numbers[0]
total = 0
for num in numbers:
total += num
if num > max_value:
max_value = num
if num < min_value:
min_value = num
average = total / len(numbers)
print(max_value)
print(min_value)
print(average)
此程序只需要遍历列表一次。
时间复杂度
对于包含 n 个元素的列表,循环执行 n 次。
时间复杂度:
O(n)
额外只需要几个变量,因此额外空间复杂度:
O(1)
3.6 列表切片 Slicing
切片可以一次取得列表中的多个元素。
基本语法:
numbers[start:stop:step]
三个参数
start:
起始索引,包含这个位置。
stop:
结束边界,不包含这个位置。
step:
索引变化步长。
例如:
numbers = [10, 20, 30, 40, 50]
print(numbers[1:4])
输出:
[20, 30, 40]
因为取索引 1、2、3,不取索引 4。
常用切片
numbers[:3] # 前三个元素
numbers[2:] # 索引 2 到末尾
numbers[-3:] # 最后三个元素
numbers[::2] # 每隔一个元素取一个
numbers[::-1] # 反转列表
为什么 [::-1] 能反转?
切片省略 start、stop,步长设为 -1。
Python 会按照负步长的默认边界,从最后一个元素开始向前访问。
因此:
numbers[::-1]
返回一个逆序的新列表。
3.7 列表推导式
列表推导式用于用简洁的表达式构建新列表。
例如:
numbers = [1, 2, 3, 4, 5, 6]
even = [num for num in numbers if num % 2 == 0]
结果:
[2, 4, 6]
完整结构:
[表达式 for 变量 in 可迭代对象 if 条件]
其执行逻辑为:
- 遍历可迭代对象。
- 检查筛选条件。
- 条件成立时计算前面的表达式。
- 将结果加入新列表。
等价的普通写法:
even = []
for num in numbers:
if num % 2 == 0:
even.append(num)
只有计算,没有筛选
squares = [i * i for i in range(1, 6)]
结果:
[1, 4, 9, 16, 25]
3.8 练习 04:切片与列表推导式
我的原始代码
numbers = [3, 8, 12, 5, 19, 24, 7, 16, 11, 30]
print(numbers[:6:-1])
print(numbers[2:7])
even = [num for num in numbers if num % 2 == 0]
print(even)
greater_ten = [i * 2 for i in numbers if i > 10]
print(greater_ten)
第一题纠错
numbers[:6:-1]
结果:
[30, 11, 16]
它是反向取得最后三个元素。
如果要求保持原来顺序:
numbers[-3:]
结果:
[16, 11, 30]
其他三题正确。
本章复习重点
- 正数与负数索引。
append()与len()。- 列表的可变性与对象引用。
- 列表切片三个参数的意义。
- 列表推导式中表达式与条件的位置。
- Python 列表与 C++ vector 的异同。
第四章:tuple、dict、set 与字符串格式化
4.1 tuple 元组
创建元组:
position = (10, 20, 30)
访问元素:
print(position[0])
输出 10。
元组与列表的主要区别是:元组不能直接改变已有元素的绑定位置。
例如:
position[0] = 100
会抛出 TypeError。
元组适合保存固定的一组值,例如坐标、配置项和多个返回结果。
需要注意,如果元组内部包含可变对象,那个对象内部的内容仍然可能变化。
4.2 dict 字典
字典保存键值对:
gpu = {
"name": "RTX 4090",
"memory": 24,
"temperature": 65
}
它可以理解为按照键查找值的数据容器。
与 C++ 的 std::unordered_map 在使用思想上有相似之处,但实现与具体语义并不完全相同。
访问
print(gpu["name"])
输出:
RTX 4090
修改
gpu["memory"] = 32
修改指定键对应的值。
增加新键值对
gpu["power"] = 450
items()
for key, value in gpu.items():
print(key, value)
items() 提供字典中的键值对。
keys() 与 values()
gpu.keys()
获取键的视图。
gpu.values()
获取值的视图。
gpu.items()
获取键值对的视图。
这些对象可以用于遍历。
4.3 set 集合
创建集合:
numbers = {1, 2, 2, 3, 3, 4}
重复的元素不会保留。
集合内容相当于:
{1, 2, 3, 4}
但集合的输出顺序不能依赖。
用途
集合适合:
- 数据去重。
- 快速判断某个元素是否存在。
- 求交集、并集和差集。
例如:
a = {1, 2, 3}
b = {3, 4, 5}
print(a & b) # 交集
print(a | b) # 并集
集合不能像列表那样使用数字索引访问。
4.4 print() 的 end 参数
Python 的 print() 默认在末尾添加换行符。
例如:
print("Hello")
print("World")
输出:
Hello
World
如果不希望换行:
print("Hello", end="")
print("World")
输出:
HelloWorld
end 用于指定输出结束时追加的内容。
4.5 f-string
Python 可以使用格式化字符串:
name = "RTX 4090"
memory = 24
print(f"{name}: {memory} GB")
输出:
RTX 4090: 24 GB
其中:
- 字符串前面的
f表示格式化字符串。 {}内可以放变量或表达式。- Python 会计算表达式,并将结果放进字符串。
例如:
print(f"2 + 3 = {2 + 3}")
输出:
2 + 3 = 5
4.6 练习 05:GPU 字典
我的原始代码
gpu = {
"RTX 4090": 24,
"RTX 4080": 16,
"RTX 4070": 12,
"RTX 4060": 8
}
print(gpu["RTX 4090"], end="")
print("GB")
for name, memory in gpu.items():
print(name, memory, end="")
print("GB")
for name, memory in gpu.items():
if memory >= 16:
print(name, memory, end="")
print("GB")
代码逻辑正确。
优化后的代码
gpu = {
"RTX 4090": 24,
"RTX 4080": 16,
"RTX 4070": 12,
"RTX 4060": 8
}
print(f"RTX 4090: {gpu['RTX 4090']} GB")
for name, memory in gpu.items():
print(f"{name}: {memory} GB")
for name, memory in gpu.items():
if memory >= 16:
print(f"{name}: {memory} GB")
本题知识点
gpu["RTX 4090"]
根据键查找值。
gpu.items()
遍历所有键值对。
for name, memory in gpu.items():
同时获取当前键与对应的值。
if memory >= 16:
按照显存容量筛选。
本章复习重点
- 四种常见容器的区别。
- 字典的键值对访问方式。
items()、keys()、values()的含义。print(end="")的作用。- f-string 的语法和使用。
第五章:NumPy 与 ndarray 多维数组
5.1 NumPy 是什么?
NumPy 是 Python 科学计算的重要基础库。
导入方式:
import numpy as np
其中:
numpy是模块名称。np是我们为它指定的别名。as用于设置导入别名。
之后:
np.array()
就是通过 np 访问 NumPy 中的 array。
5.2 ndarray 与 Python list 的区别
Python 列表:
a = [1, 2, 3]
b = [4, 5, 6]
print(a + b)
输出:
[1, 2, 3, 4, 5, 6]
这里的 + 是拼接。
NumPy:
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a + b)
输出:
[5 7 9]
这里的 + 是逐元素加法。
为什么 NumPy 适合数值计算?
NumPy 的 ndarray 通常保存同一种数据类型的元素,并利用底层优化的数值计算实现执行数组操作。
与大量 Python 层的逐元素循环相比,这种方式通常更加高效。
这叫作向量化运算。
5.3 ndim:数组维度数量
例如:
a = np.array([1, 2, 3])
一维数组:
a.ndim # 1
二维数组:
b = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(b.ndim) # 2
三维数组:
c = np.zeros((3, 2, 2))
print(c.ndim) # 3
注意,数组的维度是指索引轴数量,不一定代表真实空间维度。
5.4 shape:数组形状
shape 表示每个维度的长度。
一维:
a = np.array([1, 2, 3])
print(a.shape)
结果:
(3,)
二维:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(a.shape)
结果:
(2, 3)
表示两行三列。
三维:
a = np.zeros((3, 2, 2))
形状:
(3, 2, 2)
三个轴长度分别为 3、2、2。
5.5 dtype:元素数据类型
使用:
print(a.dtype)
可以查看数组中元素的数值类型。
常见 NumPy 类型:
np.int32
np.int64
np.float32
np.float64
例如:
a = np.array([1, 2, 3], dtype=np.float32)
创建一个元素类型为 float32 的数组。
注意:不同操作系统和 NumPy 版本中,默认整数类型可能存在差异,因此需要时应该明确指定 dtype。
5.6 多维索引
例如:
a = np.array([
[10, 20, 30],
[40, 50, 60]
])
访问第二行第三列:
print(a[1, 2])
输出:
60
Python 索引从 0 开始。
NumPy 推荐使用:
a[1, 2]
而普通嵌套列表通常使用:
a[1][2]
5.7 练习 06:ndarray 基础
我的代码
import numpy as np
a = np.array([
[3, 7, 2],
[9, 5, 8],
[4, 6, 1]
])
print(a.ndim)
print(a.shape)
print(a[1, 2])
print(a * 2)
运行结果
2
(3, 3)
8
[[ 6 14 4]
[18 10 16]
[ 8 12 2]]
全部正确。
本题总结
已经掌握:
- 使用
np.array()创建数组。 - 通过
.ndim查看维度。 - 通过
.shape查看形状。 - 使用二维索引访问元素。
- 使用
*实现逐元素运算。
第六章:NumPy 广播机制 Broadcasting
6.1 广播是什么?
广播允许形状不同、但满足兼容规则的数组进行逐元素运算。
例如:
a = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
b = np.array([10, 20, 30])
print(a + b)
结果:
[[11 22 33]
[14 25 36]
[17 28 39]]
可以理解为:
第一行:
[1, 2, 3] + [10, 20, 30]
第二行:
[4, 5, 6] + [10, 20, 30]
第三行:
[7, 8, 9] + [10, 20, 30]
每一行都与 b 对应位置相加。
6.2 广播规则
从 shape 的最右侧维度开始逐一比较。
两个对应维度能够广播,需要满足:
- 两个维度相同。
- 或者其中一个维度长度为 1。
如果维度数量不同,可以将较短 shape 的左侧视为补了 1。
例如:
A: (3, 3)
B: (3,)
可以理解为:
A: (3, 3)
B: (1, 3)
两个维度逐一比较:
- 最右侧:3 和 3,相同。
- 左侧:3 和 1,可以广播。
结果:
(3, 3)
不能广播的情况
例如:
A: (3, 2)
B: (3,)
右侧对应维度为 2 和 3。
既不相同,也没有一个为 1,因此无法广播。
会触发 ValueError。
6.3 行向量与列向量的广播
这一点比较容易混淆。
假设:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
a.shape = (2, 3)。
每一行加上同一个向量
b = np.array([10, 20, 30])
b.shape = (3,)。
print(a + b)
结果:
[[11 22 33]
[14 25 36]]
每一行分别加上不同的值
b = np.array([
[10],
[20]
])
b.shape = (2, 1)。
print(a + b)
结果:
[[11 12 13]
[24 25 26]]
可以理解为:
- 第一行全部加 10。
- 第二行全部加 20。
6.4 广播不是复制数组
在概念上,可以把广播理解为将某个维度扩展。
但实际计算时,NumPy 通常不需要完整复制出一个扩展后的数组。
这是它进行高效数值计算的重要特性之一。
6.5 练习 07:广播运算
我的原始代码
import numpy as np
a = np.array([
[3, 7, 2],
[9, 5, 8],
[4, 6, 1]
])
b = np.array([
[1, 2, 3]
])
c = a + b
print(c)
print(a.shape)
运行结果
[[ 4 9 5]
[10 7 11]
[ 5 8 4]]
(3, 3)
计算正确。
第一处需要注意的地方
b = np.array([[1, 2, 3]])
这里的 shape 是:
(1, 3)
而不是:
(3,)
两者维度数量不同。
第二处需要修改的地方
原代码:
print(a.shape)
题目要求输出结果 c 的形状。
正确写法:
print(c.shape)
这次两者结果相同,但习惯上应该验证真正的运算结果。
本章复习重点
- 广播从最右侧维度比较。
- 长度相等或其中一个长度为 1 才兼容。
(3,)与(1,3)不是同一种 shape。- 广播是逐元素运算中的形状兼容机制。
- 广播通常不需要真的复制大量数据。
第七章:reshape、矩阵乘法与 Axis
7.1 reshape() 改变数组形状
a = np.array([1, 2, 3, 4, 5, 6])
b = a.reshape(2, 3)
结果:
[[1 2 3]
[4 5 6]]
原来:
shape = (6,)
现在:
shape = (2, 3)
reshape 的要求
改变形状时,元素总数量必须保持不变。
例如有 6 个元素:
a.reshape(2, 3) # 可以
a.reshape(3, 2) # 可以
a.reshape(6, 1) # 可以
a.reshape(3, 3) # 不可以
因为:
2 × 3 = 6
3 × 2 = 6
6 × 1 = 6
3 × 3 = 9
最后一种需要 9 个元素,原数组只有 6 个。
注意:reshape 可能返回共享底层数据的视图,也可能需要复制,具体取决于内存布局。
7.2 逐元素乘法与矩阵乘法
NumPy 使用 * 表示逐元素乘法。
例如:
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6],
[7, 8]
])
print(a * b)
结果:
[[ 5 12]
[21 32]]
对应位置相乘:
1×5=5
2×6=12
3×7=21
4×8=32
矩阵乘法 @
print(a @ b)
结果:
[[19 22]
[43 50]]
计算规则是左矩阵的行与右矩阵的列做点积。
例如:
19 = 1×5 + 2×7
22 = 1×6 + 2×8
43 = 3×5 + 4×7
50 = 3×6 + 4×8
维度规则
假设:
A 的 shape 为:
(m, n)
B 的 shape 为:
(n, p)
那么 A @ B 的 shape 为:
(m, p)
也就是:
(m,n) @ (n,p) → (m,p)
内侧维度必须一致。
7.3 练习 08:reshape 与矩阵乘法
我的代码
import numpy as np
a = np.array([1, 2, 3, 4, 5, 6])
re = a.reshape(2, 3)
b = np.array([
[3, 2],
[1, 2],
[9, 5]
])
c = re @ b
print(re)
print(c)
print(c.shape)
运行结果
[[1 2 3]
[4 5 6]]
[[32 21]
[71 48]]
(2, 2)
本题完全正确。
计算形状:
(2, 3) @ (3, 2) = (2, 2)
7.4 Axis:轴
这是今天重点讨论、也比较容易产生误解的知识点。
假设:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
shape = (2, 3)。
NumPy 为每个维度编号。
axis=0
第一个轴。
对于二维数组,可以理解为沿着行索引变化的方向,即上下方向。
执行:
np.sum(a, axis=0)
得到:
[5 7 9]
具体计算:
1 + 4 = 5
2 + 5 = 7
3 + 6 = 9
也就是固定列索引,沿不同的行进行计算。
axis=1
第二个轴。
对应列索引变化的方向,即左右方向。
执行:
np.sum(a, axis=1)
得到:
[ 6 15]
具体计算:
1 + 2 + 3 = 6
4 + 5 + 6 = 15
也就是固定行索引,沿不同的列进行计算。
为什么 axis=0 得到每一列的结果?
因为 axis 指定的是要进行归约的轴。
在 sum、mean 等归约操作中,指定的轴默认会被消除。
例如:
原 shape: (2, 3)
axis=0 → (3,)
axis=1 → (2,)
最重要的理解方法:不是看最后结果有几行或几列,而是看哪个维度被消除了。
7.5 一维数组与二维数组
今天重点纠正过这个概念。
一维数组
a = np.array([1, 2, 3])
shape:
(3,)
ndim = 1。
它只有一个轴,长度为 3。
一行三列的二维数组
a = np.array([[1, 2, 3]])
shape:
(1, 3)
ndim = 2。
三行一列的二维数组
a = np.array([
[1],
[2],
[3]
])
shape:
(3, 1)
ndim = 2。
重点
不能仅根据数字在屏幕上的横向或纵向排列,判断数组维度。
应该通过数组结构、ndim 和 shape 判断。
7.6 keepdims
keepdims 用于控制归约之后是否保留被计算的轴。
默认值:
keepdims=False
例如:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
执行:
np.sum(a, axis=1)
结果:
[ 6 15]
shape:
(2,)
而:
np.sum(a, axis=1, keepdims=True)
结果:
[[ 6]
[15]]
shape:
(2, 1)
原因:
False:删除归约轴。True:保留归约轴,但长度变成 1。
为什么 keepdims 有用?
保留维度能够让后续的广播更加方便。
例如:
a = np.array([
[1., 2., 3.],
[4., 5., 6.]
])
row_mean = np.mean(a, axis=1, keepdims=True)
result = a - row_mean
此时:
a.shape = (2, 3)
row_mean.shape = (2, 1)
通过广播,能够从每一行的三个元素中减去该行的平均值。
这个操作以后在神经网络数据处理中很常见。
7.7 NumPy 常用数学函数
Python 内置函数
sum(numbers)
min(numbers)
max(numbers)
len(numbers)
这些不需要导入 NumPy。
NumPy 函数
np.sum(a)
np.mean(a)
np.max(a)
np.min(a)
其中 np.mean() 是 NumPy 提供的函数,并不是 Python 内置函数。
Python 标准库的 statistics 模块也有平均值函数:
from statistics import mean
常用 NumPy 数学函数表
np.sum(a)求和np.mean(a)平均值np.max(a)最大值np.min(a)最小值np.std(a)标准差np.var(a)方差np.sqrt(a)平方根np.abs(a)绝对值np.exp(a)指数函数np.log(a)自然对数np.sin(a)正弦函数np.cos(a)余弦函数这些函数中的许多都支持使用 axis 指定计算轴。
对象方法写法
例如:
np.mean(a)
也可以写成:
a.mean()
前者调用 NumPy 函数。
后者调用 ndarray 对象自身的方法。
7.8 今日关于 mean 的错误
曾经认为:
np.mean(a, axis=1)
得到每行求和结果。
但 mean 的含义是平均值,不是求和。
例如:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
b = np.mean(a, axis=1, keepdims=True)
第一行:
(1 + 2 + 3) / 3 = 2
第二行:
(4 + 5 + 6) / 3 = 5
最终:
[[2.]
[5.]]
shape:
(2, 1)
本章需要重点复习 axis、shape 和 keepdims 三者之间的关系。
第八章:Python 类与面向对象编程
8.1 class:定义类
Python:
class GPU:
pass
class 用于定义类。
pass 表示空操作,可以在暂时没有具体实现时占位。
对应 C++:
class GPU
{
};
8.2 init:初始化方法
Python:
class GPU:
def __init__(self, name, memory):
self.name = name
self.memory = memory
创建对象:
gpu = GPU("RTX 4090", 24)
创建实例时,Python 会调用初始化方法。
但严格来说,__init__ 是初始化已有新实例的方法,而对象的创建还涉及 __new__。
入门阶段把它类比为 C++ 构造函数的初始化作用即可。
8.3 self:当前对象
例如:
self.name = name
右边的 name 是传入的参数。
左边的 self.name 是当前对象的实例属性。
例如:
gpu1 = GPU("RTX 4090", 24)
gpu2 = GPU("RTX 4080", 16)
两个对象分别保存自己的数据。
self 与 C++ this
C++:
this->memory = memory;
Python:
self.memory = memory
虽然概念类似,但 C++ 的 this 是指针,Python 的 self 是对象引用。
为什么调用时不用写 self?
定义:
def show_info(self):
调用:
gpu.show_info()
Python 会自动将调用该方法的实例作为第一个参数。
从效果上可以理解为:
GPU.show_info(gpu)
8.4 练习 10:Renderer 类
我的原始代码
class Renderer:
def __init__(self, name, width):
self.name = name
self.width = width
def show_info(self)
print(f"{self.name}{self.width}")
R1 = Renderer("Eidovelis", 1920)
R1.show_info()
错误一:缺少冒号
def show_info(self)
应该写:
def show_info(self):
错误二:函数体缩进错误
print() 应该属于 show_info() 函数体,因此必须再缩进一级。
修改后的代码
class Renderer:
def __init__(self, name, width):
self.name = name
self.width = width
def show_info(self):
print(f"Renderer: {self.name}")
print(f"Width: {self.width}")
R1 = Renderer("Eidovelis", 1920)
R1.show_info()
运行结果:
Renderer: Eidovelis
Width: 1920
8.5 继承 Inheritance
Python:
class Renderer:
def show(self):
print("普通渲染器")
class NeuralRenderer(Renderer):
pass
其中:
class NeuralRenderer(Renderer):
表示 NeuralRenderer 继承 Renderer。
如果子类没有重写 show(),就可以使用继承来的父类方法。
8.6 方法重写 Override
class Renderer:
def show(self):
print("普通渲染器")
class NeuralRenderer(Renderer):
def show(self):
print("神经渲染器")
调用:
r = NeuralRenderer()
r.show()
输出:
神经渲染器
这是方法重写。
与 C++ 不同,Python 普通实例方法默认支持动态分派,不需要 virtual 关键字。
8.7 super()
super() 用于按照 Python 的方法解析顺序访问继承体系中的方法。
简单单继承情况下:
class Renderer:
def __init__(self, name):
self.name = name
class NeuralRenderer(Renderer):
def __init__(self, name, quality):
super().__init__(name)
self.quality = quality
其中:
super().__init__(name)
调用父类的初始化方法。
作用是复用已经存在的初始化逻辑,避免重复编写。
注意:在多重继承中,super() 遵循 MRO(方法解析顺序),不一定直接指向某个固定父类。
8.8 练习 11:GPU 类继承
我的原始代码
class GPU:
def __init__(self, name):
self.name = name
def show_info(self):
print(f"GPU:{self.name}")
class NvidiaGPU(GPU):
def __init__(self, name, memory):
super().__init__(name)
self.memory = memory
def show_info(self):
print(f"GPU:{self.name}")
print(f"memory:{self.memory}GB")
gpu = NvidiaGPU("RTX 4090", 24)
gpu.show_info()
运行结果
GPU:RTX 4090
memory:24GB
代码逻辑正确。
输出格式优化
可以改成:
def show_info(self):
print(f"GPU: {self.name}")
print(f"Memory: {self.memory} GB")
本题掌握
- 父类创建。
- 子类继承。
- 使用
super()进行初始化。 - 增加子类实例属性。
- 重写实例方法。
- 实例化子类并调用方法。
与 PyTorch 的联系
定义 PyTorch 神经网络时会遇到:
class CNN(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return x
其中:
nn.Module
是神经网络模块基类。
掌握 Python 类与继承之后,后续学习这种结构会容易很多。
第九章:模块与异常处理
9.1 什么是模块 Module?
Python 中一个 .py 文件通常就可以作为一个模块。
例如项目目录:
PythonProject/
math_utils.py
main.py
math_utils.py:
def add(a, b):
return a + b
main.py:
import math_utils
result = math_utils.add(10, 20)
print(result)
输出:
30
import 的作用
import math_utils
导入模块。
然后通过:
math_utils.add()
调用模块里的函数。
from import
from math_utils import add
直接导入指定名称。
之后:
add(10, 20)
不再需要加 math_utils.。
as
import numpy as np
表示将导入的 numpy 模块绑定到别名 np。
9.2 try...except 异常处理
Python 可能在运行时发生异常。
例如:
a = 10
b = 0
result = a / b
这里会触发:
ZeroDivisionError
因为除数不能为 0。
使用异常处理:
try:
result = a / b
except ZeroDivisionError:
print("除数不能为0")
可以捕获这个异常。
try 的执行过程
- Python 尝试执行
try中的代码。 - 如果出现异常,停止执行该
try块中尚未执行的语句。 - 寻找匹配的
except。 - 执行对应的异常处理代码。
- 如果异常已经处理,继续执行整个结构后面的代码。
常见异常
ValueError参数值不符合要求TypeError类型或操作不匹配IndexError序列索引越界KeyError字典缺少指定键ZeroDivisionError除以零FileNotFoundError文件不存在NameError名称未定义SyntaxErrorPython 语法错误9.3 input() 和 int()
input()
name = input("请输入名字:")
读取用户输入。
返回值是字符串。
即使用户输入:
10
返回的也是字符串 "10"。
int()
a = int("10")
将字符串转换为整数。
如果输入不符合整数格式:
int("abc")
会触发 ValueError。
9.4 练习 12:处理列表索引输入
我的原始代码
numbers = [10, 20, 30]
try:
index = int(input("请输入索引:"))
result = index
print(result)
except ValueError:
print("请输入整数")
except IndexError:
print("索引超出范围")
主要错误
result = index
这里只是将索引值赋给 result。
假如输入:
1
就输出 1,而不是列表中的 20。
正确做法:
result = numbers[index]
通过索引访问列表。
修改后的代码
numbers = [10, 20, 30]
try:
index = int(input("请输入索引:"))
result = numbers[index]
print(result)
except ValueError:
print("请输入整数")
except IndexError:
print("索引超出范围")
三种执行情况
输入:
1
正常输出:
20
输入:
abc
由于 int() 转换失败,触发 ValueError。
程序输出:
请输入整数
输入:
5
由于 numbers[5] 越界,触发 IndexError。
程序输出:
索引超出范围
注意:Python 允许负数索引,因此 -1 对这个列表是合法的。
9.5 异常处理执行顺序
今天完成过一道判断题:
numbers = [10, 20, 30]
try:
print(numbers[5])
except ValueError:
print("A")
except IndexError:
print("B")
print("C")
正确输出:
B
C
原因:
- 列表访问越界。
ValueError不匹配。IndexError匹配,输出 B。- 异常得到处理。
- 继续执行后面的
print("C")。
第十章:PyTorch Tensor 入门
10.1 PyTorch 是什么?
PyTorch 是主要面向机器学习与深度学习的计算框架。
它提供:
- Tensor 张量运算。
- GPU 加速计算。
- 自动微分系统。
- 神经网络模块。
- 优化器及训练相关工具。
今天只学习了 Tensor 的基础操作,尚未正式学习自动微分。
10.2 Tensor 是什么?
Tensor 可以先理解为多维数组。
NumPy:
import numpy as np
a = np.array([
[1, 2],
[3, 4]
])
PyTorch:
import torch
a = torch.tensor([
[1, 2],
[3, 4]
])
两者结构类似。
创建一维 Tensor
a = torch.tensor([1, 2, 3])
shape:
torch.Size([3])
创建二维 Tensor
a = torch.tensor([
[1, 2, 3],
[4, 5, 6]
])
shape:
torch.Size([2, 3])
10.3 Tensor 的三个重要属性
shape
a.shape
用于查看 Tensor 的形状。
dtype
a.dtype
用于查看 Tensor 的元素类型。
常见类型:
torch.int64
torch.float32
torch.float64
例如:
a = torch.tensor(
[1, 2, 3],
dtype=torch.float32
)
这里显式要求创建浮点 Tensor。
device
a.device
用于查看 Tensor 所在的计算设备。
例如:
cpu
或:
cuda:0
具体取决于 Tensor 所在的设备。
10.4 CPU 与 GPU
Tensor 不仅可以放在 CPU 上计算,也可以放在支持的 GPU 上计算。
例如,在 CUDA 环境可用时:
a = a.to("cuda")
将 Tensor 转移至 CUDA GPU。
不过需要确保安装了适合当前 NVIDIA 显卡和软件环境的 PyTorch 构建。
如果 CUDA 不可用,直接移动到 CUDA 设备会报错。
10.5 Tensor 的数学运算
与 NumPy 类似:
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = a + b
结果:
tensor([5, 7, 9])
也支持标量运算:
a * 3
以及矩阵乘法:
a @ b
对于二维 Tensor,@ 执行矩阵乘法。
10.6 torch.mean()
计算 Tensor 的平均值。
a = torch.tensor([1., 2., 3.])
print(torch.mean(a))
结果:
tensor(2.)
也可以使用对象方法:
a.mean()
注意整数类型
例如:
a = torch.tensor([1, 2, 3])
默认通常得到整数 Tensor。
直接执行:
torch.mean(a)
会因输入不是浮点或复数 Tensor 而报错。
可以改成:
a = torch.tensor(
[1, 2, 3],
dtype=torch.float32
)
也可以:
a = a.float()
将数据转换成浮点类型。
10.7 练习 13:第一个 Tensor
我的原始代码
import torch
a = torch.tensor([
[2, 5, 8],
[4, 5, 8],
[9, 5, 8]
])
b = a * 3
print(b.shape)
print(b.dtype)
print(b.device)
问题一:shape 不符合要求
创建的是:
(3, 3)
而题目要求:
(2, 3)
因此要保留两行、每行三个元素。
问题二:遗漏 mean
题目要求计算平均值。
应使用:
torch.mean(b)
问题三:整数 dtype
由于创建 Tensor 时使用了整数,默认类型通常是:
torch.int64
需要改成浮点 Tensor 才能使用 torch.mean()。
修改后的代码
import torch
a = torch.tensor([
[2, 5, 8],
[4, 5, 8]
], dtype=torch.float32)
b = a * 3
average = torch.mean(b)
print(b)
print(average)
print(b.shape)
print(b.dtype)
print(b.device)
运行结果
在 CPU 环境下:
tensor([[ 6., 15., 24.],
[12., 15., 24.]])
tensor(16.)
torch.Size([2, 3])
torch.float32
cpu
本题掌握的知识
- 使用
torch.tensor()创建 Tensor。 - 使用
dtype指定元素类型。 - 使用
*进行逐元素运算。 - 使用
torch.mean()求平均值。 - 查看
shape、dtype、device。
10.8 NumPy 与 PyTorch 对照表
np.array()torch.tensor()创建数组或张量.shape.shape获取形状.ndim.ndim获取维度数量.dtype.dtype获取数据类型.reshape().reshape()改变形状np.sum()torch.sum()求和np.mean()torch.mean()求平均值np.max()torch.max()求最大值a @ ba @ b矩阵乘法axis=0dim=0dim=0指定归约维度PyTorch 的两个重要扩展能力
GPU 运算:
Tensor 可以方便地参与 CUDA 等 GPU 后端的运算。
自动微分:
PyTorch 提供 Autograd 系统,用于计算梯度。
后续会学习:
requires_grad=True
backward()
tensor.grad
这些目前只了解用途,还没有正式进行编程练习。
第十一章:今日错误与纠正汇总
for i in (1,n+1)把元组误当连续范围使用 range()偶数求和每次加 2没有累加当前偶数total += i最大值初始值设为 0无法正确处理全负数使用首元素初始化for num in numbers,:逗号使遍历对象变为单元素元组删除逗号把变量命名为 sum遮蔽 Python 内置函数使用 totalnumbers[:6:-1]输出顺序和题目要求不同使用 numbers[-3:]把 (3,) 当成 (1,3)没有区分一维和二维查看 ndim 与 shape对 axis 理解不准确混淆归约方向和结果排列看哪个维度被消除把 mean 当 sum混淆平均值与求和平均值需要除以数量忘记函数冒号Python 语法不熟悉def func():函数体缩进错误没有正确划分作用域函数体再缩进一级result = index只保存索引,没有访问列表result = numbers[index]Tensor shape 不符合要求创建时没有检查维度长度使用 .shape 验证整数 Tensor 求 mean输入 dtype 不符合要求转换为浮点 Tensor第十二章:复习速查表
Python 基础
# 变量
a = 10
# 输出
print(a)
# 不换行
print(a, end="")
# 条件
if a > 10:
pass
elif a == 10:
pass
else:
pass
# 循环
for i in range(5):
print(i)
# while
while a > 0:
a -= 1
# 函数
def add(a, b):
return a + b
# 类型
type(a)
# 输入
input("请输入:")
# 整数转换
int("10")
Python 数据结构
# 列表
numbers = [1, 2, 3]
numbers.append(4)
numbers[0]
numbers[-1]
numbers[1:3]
numbers[::-1]
len(numbers)
# 元组
position = (1, 2, 3)
# 字典
gpu = {"RTX 4090": 24}
gpu["RTX 4090"]
gpu.items()
gpu.keys()
gpu.values()
# 集合
data = {1, 2, 3}
# 推导式
[i * 2 for i in numbers if i > 1]
NumPy
import numpy as np
a = np.array([
[1, 2],
[3, 4]
])
a.ndim
a.shape
a.dtype
a[0, 1]
a.reshape(1, 4)
a * 2
a @ a
np.sum(a)
np.mean(a)
np.max(a)
np.min(a)
np.sum(a, axis=0)
np.mean(a, axis=1, keepdims=True)
Python 类
class GPU:
def __init__(self, name):
self.name = name
def show(self):
print(self.name)
class NvidiaGPU(GPU):
def __init__(self, name, memory):
super().__init__(name)
self.memory = memory
def show(self):
print(self.name, self.memory)
异常处理
try:
index = int(input("索引:"))
print(numbers[index])
except ValueError:
print("输入不是整数")
except IndexError:
print("索引越界")
PyTorch
import torch
a = torch.tensor([
[1, 2, 3],
[4, 5, 6]
], dtype=torch.float32)
a.shape
a.dtype
a.device
b = a * 2
torch.mean(b)
torch.sum(b)
b.reshape(3, 2)
第十三章:学习进度与后续计划
目前已经学习
Python
- 基础数据类型
- 变量与运算符
- 条件判断与循环
- 函数、参数、return
- 列表、切片、推导式
- 元组、字典、集合基础
- 对象引用
- 类、对象、继承、方法重写
- super()
- import
- try...except
- input()
NumPy
- ndarray
- ndim、shape、dtype
- 多维索引
- 向量化运算
- 广播机制
- reshape
- 矩阵乘法
- axis
- keepdims
- 常用统计函数
PyTorch
- Tensor 创建
- shape、dtype、device
- 基础逐元素运算
- torch.mean()
尚未系统学习
Python 补充:
文件读写、包的组织、Python 环境管理等内容。
NumPy 补充:
多维切片、布尔索引、transpose、数组拼接、视图与复制等内容。
PyTorch 后续重点:
- Autograd 自动微分。
requires_grad。backward()。- 梯度与链式法则。
- 损失函数。
- 优化器。
nn.Module。- 神经网络训练循环。
- CNN 的 PyTorch 实现。
- 图像降噪训练项目。
下一次学习起点
PyTorch 第 2 课:自动微分 Autograd
学习顺序:
导数与梯度 → requires_grad → 计算图 → backward() → .grad → 简单梯度下降。
下一阶段不只要求程序能够运行,还需要能够解释每一行代码在神经网络训练中的作用。
今日总结: 已经从 Python 基础语法学习到 NumPy 的多维数值运算,并开始掌握 PyTorch Tensor。下一阶段将从普通的张量计算过渡到自动微分和神经网络训练。
本记录同时作为后续复习材料,重点回顾练习中的错误及其产生原因。