很多人学NumPy只看代码不看文字,结果一知半解。本文反其道而行:每个代码示例前先讲“为什么”,代码后再说“是什么”,让你真正理解每一条指令。
一、NumPy是什么?为什么必须学?
NumPy是Python科学计算的基石。它的核心是一个叫做ndarray的多维数组对象。为什么不用Python自带的列表?因为列表里的元素可以是不同类型,每个元素都是一个独立的对象,内存不连续,循环遍历很慢。而NumPy数组所有元素类型相同,内存连续排列,并且大部分运算都是用C语言实现的,速度可以比列表快几十倍甚至上百倍。
下面我们创建第一个NumPy数组,看看它和列表有什么不同。
import numpy as np# 从普通列表创建NumPy数组py_list = [1, 2, 3]np_array = np.array(py_list)print(py_list) # [1, 2, 3] 逗号分隔print(np_array) # [1 2 3] 没有逗号,这是NumPy的打印风格print(type(np_array)) # <class 'numpy.ndarray'>
可以看到,NumPy数组的打印结果里元素之间只有空格,没有逗号。这个小小的视觉差异其实反映了底层存储的不同:列表是一个个Python对象指针的序列,而数组是一块连续的内存区域。
二、ndarray的三个“限制”(其实是性能的代价)
初学NumPy时,你可能会觉得它很死板。下面三个限制是初学者最容易抱怨的,但理解它们背后的原因,你就知道为什么NumPy这么快了。
限制一:所有元素必须类型相同
如果数组中混入了一个浮点数,整个数组都会变成浮点类型。这样做的好处是每个元素占用的字节数一致,CPU可以快速定位到第i个元素的地址:起始地址 + i × 元素字节数。
# 尝试混合整数和浮点数mixed = np.array([1, 2, 3.5])print(mixed) # [1. 2. 3.5] 所有整数都变成了浮点数print(mixed.dtype) # float64
限制二:数组总大小创建后不可变
你不能像列表那样append或pop。这是为了保持内存的连续性。如果需要动态增删元素,要么提前分配足够大的空间,要么先用列表收集数据最后再转成数组。
arr = np.array([1,2,3])# arr.append(4) # 错误!ndarray没有append方法# 正确做法:先收集到列表,再转换data = [1,2,3]data.append(4)arr = np.array(data)print(arr) # [1 2 3 4]
限制三:形状必须是矩形
二维数组的每一行必须有相同数量的列,不能出现“锯齿”。因为NumPy的索引计算依赖于规整的形状:访问(i,j)元素的偏移量 = i * 列数 + j。
# 下面这个会报错# bad = np.array([[1,2], [3,4,5]]) # ValueError: setting an array element with a sequence# 矩形数组没问题good = np.array([[1,2,3], [4,5,6]])print(good.shape) # (2, 3)
三、必须掌握的数组属性(像体检报告一样重要)
拿到一个数组,首先要查看它的“体检报告”。下面这五个属性是最常用的。
a = np.array([[1, 2, 3], [4, 5, 6]])# 维度数(几维数组)print(a.ndim) # 2# 形状(各维度的长度)print(a.shape) # (2, 3)# 元素总个数print(a.size) # 6# 元素的数据类型print(a.dtype) # int64# 每个元素占用的字节数print(a.itemsize) # 8 (因为int64是8字节)# 数组总共占用的字节数(等于 size * itemsize)print(a.nbytes) # 48
当你拿到一个陌生的数组时,先用这些属性看看它的结构,可以避免很多错误。
四、创建数组的多种方法(不同场景用不同函数)
1. array 与 asarray:拷贝还是共享?
np.array()总是会复制输入数据,生成一个全新的数组。而np.asarray()如果输入已经是ndarray,就不会复制,直接共享内存。这在你需要避免不必要的内存复制时很有用。
original = np.array([1, 2, 3])# 使用 array:总是拷贝copy_arr = np.array(original)copy_arr[0] = 999print(original[0]) # 1 (原数组没变)# 使用 asarray:如果输入是ndarray,则共享内存view_arr = np.asarray(original)view_arr[0] = 888print(original[0]) # 888 (原数组也被改了)
如果输入是一个列表,asarray也会拷贝,因为列表不是ndarray。
lst = [1,2,3]arr1 = np.asarray(lst) # 这里会拷贝,因为输入是列表arr1[0] = 777print(lst[0]) # 1 (列表没变)
2. zeros, ones, empty 以及它们的 _like 版本
创建全0数组、全1数组是非常常见的需求,比如初始化权重矩阵。
# 全0数组,默认float64类型zeros_arr = np.zeros((2, 3))print(zeros_arr)# [[0. 0. 0.]# [0. 0. 0.]]# 全1数组ones_arr = np.ones((2, 3))print(ones_arr)# [[1. 1. 1.]# [1. 1. 1.]]# empty数组:只分配内存,不初始化,里面的值是随机的(内存垃圾)empty_arr = np.empty((2, 3))print(empty_arr)# 运行结果每次都可能不同,比如:# [[-9.05243306e-312 1.06658093e-264 9.05246807e-312]# [ 9.05246807e-312 6.91691904e-323 2.96439388e-323]]
empty比zeros快,因为它不需要花时间把每个元素设为0。但你必须确保之后会覆盖所有元素,否则读取到未初始化的垃圾值会导致难以调试的问题。
_like系列函数可以基于已有数组的形状和类型创建新数组,非常方便。
template = np.array([[1,2],[3,4]])like_zeros = np.zeros_like(template) # 形状和类型与template相同like_ones = np.ones_like(template)print(like_zeros) # [[0 0] [0 0]] 类型是int64,与template一致
3. full:用指定值填充
有时候你需要所有元素都是同一个非0非1的值,比如初始化偏置项为0.5。
full_arr = np.full((2, 4), 7) # 2行4列,每个元素都是7print(full_arr)# [[7 7 7 7]# [7 7 7 7]]
4. arange:类似range的数组版本
np.arange()和Python内置的range()用法几乎一样,但它返回的是ndarray而不是迭代器。
print(np.arange(5)) # [0 1 2 3 4]print(np.arange(2, 10, 2)) # [2 4 6 8]print(np.arange(0, 1, 0.3)) # [0. 0.3 0.6 0.9] 支持浮点步长
注意:当使用浮点步长时,由于浮点精度问题,arange可能包含的元素个数不是精确预期的。更安全的做法是用linspace。
5. linspace 和 logspace:等间隔与等比间隔
linspace在指定的区间内生成固定数量的等间距点,并且可以控制是否包含终点。这在绘图时非常有用。
# 从0到10,生成5个点(包含10)linear = np.linspace(0, 10, 5)print(linear) # [ 0. 2.5 5. 7.5 10. ]# 不包含终点的情况linear_no_end = np.linspace(0, 10, 5, endpoint=False)print(linear_no_end) # [0. 2. 4. 6. 8.]# 这里间隔 = (10-0)/5 = 2,所以得到0,2,4,6,8
logspace用于生成等比数列。它接收指数范围的起点和终点,以及底数。
# 底数为2,从2^2=4到2^5=32,取5个数log_vals = np.logspace(2, 5, 5, base=2)print(log_vals)# [ 4. 6.72717132 11.3137085 19.02731384 32. ]# 可以看到,这些数在对数坐标上是均匀的
6. 随机数数组:模拟数据必备
NumPy的random模块提供了多种随机分布,可以用来生成测试数据或进行蒙特卡洛模拟。
# 在[0,1)区间均匀分布,形状(2,3)rand_uniform = np.random.rand(2, 3)print(rand_uniform)# 例如:[[0.77112868 0.97415392 0.25668864]# [0.49946961 0.23491874 0.40514576]]# 随机整数,从0到10(不含10),形状(2,3)rand_int = np.random.randint(0, 10, (2, 3))print(rand_int)# 例如:[[7 8 2] [1 2 3]]# 在[3,6)区间均匀分布的浮点数rand_uniform_range = np.random.uniform(3, 6, (2, 3))print(rand_uniform_range)# 例如:[[5.69275495 3.84857937 3.2899215] [5.32035519 3.7460973 3.33859905]]# 标准正态分布 N(0,1)rand_normal = np.random.randn(2, 3)print(rand_normal)# 例如:[[-2.03654925 -0.50146561 0.4362483] [-1.90585739 0.94797017 -0.77026926]]
五、数据类型与转换(精确控制内存与精度)
创建数组时可以通过dtype参数指定类型。默认整数是int64,浮点是float64。如果你处理的数据量很大,可以使用float32来节省一半内存。
# 创建时指定类型arr_f32 = np.array([1, 2, 3], dtype=np.float32)print(arr_f32.dtype) # float32# 使用类型代码arr_i8 = np.array([1,2,3], dtype='i8') # i8 代表 int64print(arr_i8.dtype) # int64
astype方法可以转换已有数组的类型。注意:浮点数转整数时会截断小数部分,不是四舍五入。
float_arr = np.array([1.9, 2.7, 3.2])int_arr = float_arr.astype(np.int64)print(int_arr) # [1 2 3] 小数部分直接丢弃# 如果需要四舍五入,先用 np.rintrounded = np.rint(float_arr).astype(np.int64)print(rounded) # [2 3 3]
六、切片和索引(小心视图!)
NumPy的切片语法和Python列表很像,但有一个关键区别:切片返回的是原数组的视图(view),而不是副本。这意味着修改切片会改变原数组。
# 一维数组切片arr = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]print(arr[2]) # 2print(arr[2:9:2]) # [2 4 6 8]print(arr[2:]) # [2 3 4 5 6 7 8 9]# 二维数组切片arr2d = np.array([[1,2,3], [4,5,6], [7,8,9]])print(arr2d[0, 1]) # 2 (第0行第1列)print(arr2d[:2, 1:]) # [[2 3] [5 6]]# 重要:切片是视图sub = arr2d[:2, :2] # 左上角2x2区域sub[0,0] = 999print(arr2d[0,0]) # 999 (原数组被修改了!)
如果你需要一个真正的副本,请显式调用.copy()方法。
sub_copy = arr2d[:2, :2].copy()sub_copy[0,0] = 0print(arr2d[0,0]) # 仍然是999,因为副本不影响原数组
七、常用函数(每个函数都有例子)
7.1 基本数学函数
这些函数都是逐元素操作的。
arr = np.random.randn(2, 3) # 标准正态分布随机数print("原始数组:\n", arr)# 绝对值print("绝对值:\n", np.abs(arr))# 向上取整(向正无穷)print("向上取整:\n", np.ceil(arr))# 向下取整(向负无穷)print("向下取整:\n", np.floor(arr))# 四舍五入到最近的整数print("四舍五入:\n", np.rint(arr))# 判断是否为NaNprint("是否为NaN:\n", np.isnan(arr))# 逐元素相乘a = np.array([1,2,3])b = np.array([4,5,6])print("逐元素相乘:", np.multiply(a, b)) # [4 10 18]# 逐元素相除print("逐元素相除:", np.divide(a, b)) # [0.25 0.4 0.5 ]# where:三元运算符的矢量化版本condition = np.array([-1, 2, -3, 4])result = np.where(condition > 0, 1, 0)print("where结果:", result) # [0 1 0 1]
7.2 统计函数
统计函数默认计算整个数组。通过axis参数可以指定按行或按列计算。
arr = np.array([[1,2,3], [4,5,6]])print("全局平均值:", np.mean(arr)) # 3.5print("全局和:", np.sum(arr)) # 21print("全局最大值:", np.max(arr)) # 6print("全局最小值:", np.min(arr)) # 1print("全局标准差:", np.std(arr)) # 1.707825...print("全局方差:", np.var(arr)) # 2.916666...# axis=0 表示沿着第0维(行方向)操作,即按列计算print("按列求和:", np.sum(arr, axis=0)) # [5 7 9]print("按列平均值:", np.mean(arr, axis=0)) # [2.5 3.5 4.5]# axis=1 表示沿着第1维(列方向)操作,即按行计算print("按行求和:", np.sum(arr, axis=1)) # [6 15]print("按行平均值:", np.mean(arr, axis=1)) # [2. 5.]# 最大值、最小值的索引(全局)print("全局最大值索引:", np.argmax(arr)) # 5(展平后的位置)print("全局最小值索引:", np.argmin(arr)) # 0# 累加和(返回一维数组)print("累加和:", np.cumsum(arr)) # [ 1 3 6 10 15 21]# 累乘积(按行)print("按行累乘积:\n", np.cumprod(arr, axis=1))# [[ 1 2 6]# [ 4 20 120]]
7.3 比较函数 any 和 all
arr = np.array([1,2,3,4,5])print("是否存在大于3的元素?", np.any(arr > 3)) # Trueprint("是否所有元素都大于3?", np.all(arr > 3)) # False
7.4 排序
NumPy提供了两种排序方式:原地排序和返回副本。
arr = np.random.randint(0, 10, (3, 3))print("原数组:\n", arr)# 原地排序,默认按最后一个轴(对于二维数组即按行)arr.sort()print("按行排序后(原地):\n", arr)# 按列排序(每一列独立排序)arr.sort(axis=0)print("按列排序后:\n", arr)# 不修改原数组的排序arr2 = np.random.randint(0, 10, (3, 3))sorted_arr = np.sort(arr2) # 返回新数组print("原数组不变:\n", arr2)print("排序后的新数组:\n", sorted_arr)
7.5 去重
arr = np.random.randint(0, 5, (3, 3))print("原数组:\n", arr)unique_vals = np.unique(arr)print("唯一值(已排序):", unique_vals)# 同时返回频次vals, counts = np.unique(arr, return_counts=True)print("值:", vals)print("出现次数:", counts)
八、矢量化运算与广播(NumPy性能的核心)
8.1 数组与标量运算
标量会被“广播”到数组的每个元素上。
arr = np.array([[1,2,3],[4,5,6]])print(arr + 10) # [[11 12 13] [14 15 16]]print(arr * 2) # [[ 2 4 6] [ 8 10 12]]
8.2 相同形状数组之间的逐元素运算
a = np.array([1,2,3])b = np.array([4,5,6])print(a + b) # [5 7 9]print(a * b) # [4 10 18] (注意这不是矩阵乘法)
8.3 广播:不同形状数组的运算
广播机制允许不同形状的数组进行运算,前提是它们可以“对齐”。对齐规则如下:
- 如果两个数组的维度数不同,则在维度较少的数组形状左边补1。
- 比较每个维度的大小:如果大小相同或者其中一个为1,则兼容;否则报错。
- 对于大小为1的维度,该维度会被拉伸(逻辑上复制)以匹配另一个数组的大小。
下面通过几个例子来理解。
例子1:一维数组 (3,) 与 二维数组 (3,1) 相加
row = np.array([1,2,3]) # 形状 (3,)col = np.array([[4],[5],[6]]) # 形状 (3,1)# 根据规则1:row的维度数少,左边补1,变成 (1,3)# 现在形状 (1,3) 和 (3,1)# 规则2:在维度0上,1和3,将row扩展为 (3,3)# 在维度1上,3和1,将col扩展为 (3,3)# 最终得到3x3的加法结果result = row + colprint(result)# [[5 6 7]# [6 7 8]# [7 8 9]]
例子2:行向量 (1,3) 与列向量 (2,1) 相加
row2 = np.array([[1,2,3]]) # 形状 (1,3)col2 = np.array([[4],[5]]) # 形状 (2,1)# 规则2:维度0上,1和2,将row2扩展为 (2,3)# 维度1上,3和1,将col2扩展为 (2,3)result2 = row2 + col2print(result2)# [[5 6 7]# [9 10 11]]
例子3:无法广播的情况
a = np.array([1,2,3]) # (3,)b = np.array([4,5]) # (2,)# 两个一维数组,维度数相同,直接比较维度大小:3 vs 2,不相等且没有1,所以报错# a + b # ValueError: operands could not be broadcast together with shapes (3,) (2,)
九、矩阵乘法(区分逐元素乘和真正的矩阵乘)
很多初学者会混淆*和@。记住:*是逐元素相乘,@是线性代数中的矩阵乘法。
# 逐元素乘法:形状必须相同或可广播A = np.array([[1,2],[3,4]])B = np.array([[5,6],[7,8]])print(A * B) # [[ 5 12] [21 32]] 对应位置相乘# 矩阵乘法:第一个矩阵的列数必须等于第二个矩阵的行数C = np.array([[1,2,3],[4,5,6]]) # 2x3D = np.array([[6,5],[4,3],[2,1]]) # 3x2# 三种等价的写法print(C @ D) # 推荐,最直观print(C.dot(D))print(np.dot(C, D))# 结果 [[20 14]# [56 41]]# 手动验证第一个元素:C的第一行[1,2,3]点乘D的第一列[6,4,2] = 1*6+2*4+3*2=20# 第二个元素:第一行点乘第二列[5,3,1] = 1*5+2*3+3*1=14# 第三元素:第二行点乘第一列 = 4*6+5*4+6*2=56# 第四元素:第二行点乘第二列 = 4*5+5*3+6*1=41# 二维数组与一维数组的矩阵乘法:结果是一维数组e = np.array([6,5,4]) # 长度必须等于C的列数print(C @ e) # [28 73]# 计算:第一行[1,2,3]点乘[6,5,4]=28;第二行[4,5,6]点乘[6,5,4]=73
总结
本文通过大量代码示例和逐段解释,覆盖了NumPy最核心的知识点:
- 创建数组:array、asarray、zeros、ones、empty、full、arange、linspace、logspace、随机数函数。不同场景用不同函数,可以节省内存或提高可读性。
- 数组属性:ndim、shape、size、dtype、itemsize、nbytes,拿到数组先看这些。
- 数据类型:用dtype指定,用astype转换,注意浮点转整型是截断。
- 切片与索引:切片返回视图,修改视图会影响原数组,需要副本时用.copy()。
- 常用函数:数学函数(abs、ceil、floor、rint、where),统计函数(mean、sum、max、min、std、cumsum、argmax等),比较函数(any、all),排序(sort原地、np.sort副本),去重(unique)。
- 广播机制:理解三条规则,让不同形状数组安全运算,避免报错。
- 矩阵乘法
学习建议:打开Jupyter Notebook,把本文的每个代码块手动敲一遍,修改参数看看结果变化,遇到不懂的地方再回头读解释。当你熟练了这些基础操作,就可以开始用NumPy处理真实数据,或者进一步学习Pandas、Matplotlib等库。祝你学习愉快!