AB
AiBoss
チュートリアル

NumPy

チュートリアル

用 NumPy 创建数组并完成索引、切片与形状变换

面向初学者的 NumPy 实操教程:从安装与导入开始,逐步完成数组创建、索引、切片、数据类型、副本与视图、形状变换、迭代、拼接与拆分,并给出一个从头到尾可运行的最小示例,同时说明常见坑与注意事项。

NumPy 是 Python 生态里处理数值数组的基础库,名字来自 “Numerical Python”。它解决的核心问题是:当你需要对成批的数字做同样的运算、按位置取数据、改变数据的维度结构时,纯 Python 列表写起来又长又慢,而 NumPy 提供了统一的数组对象和一套围绕它的操作函数。这篇教程面向刚开始接触 NumPy 的读者,把创建数组、索引、切片、数据类型、副本与视图、形状变换、迭代、拼接与拆分这条主线走一遍,每一步都给出可以直接运行的代码。如果你希望在一个网页环境里直接试跑这些片段,可以打开 NumPy 在线运行环境,把代码粘进去看输出,不必先在本地配环境。

准备工作

开始之前需要确认三件事:Python 环境、NumPy 包、以及一个能执行代码的地方。

Python 与 NumPy 的安装

NumPy 是第三方库,标准 Python 安装里不带它,需要单独安装。安装命令取决于你用的包管理器:

pip install numpy

如果你用的是 conda 环境,也可以用 conda 安装:

conda install numpy

安装完成后,在 Python 交互环境里验证一下能否导入:

import numpy as np
print(np.__version__)

能打印出版本号就说明安装成功。具体的版本号、各版本新增或废弃的接口,请以 NumPy 官网当前信息为准,本文不锁定版本。

导入约定

社区里几乎所有人都把 NumPy 导入为 np,后面的示例统一使用这个别名:

import numpy as np

用别名不是语法要求,但读别人的代码、查资料时到处都会看到 np. 前缀,跟着这个约定走能省掉很多对照成本。

选择运行方式

有三种常见做法:本地装好 NumPy 后用 Python 脚本或 Jupyter 运行;在支持 NumPy 的在线编辑器里直接写代码点运行;或者把片段存成 .py 文件用 python 文件名.py 执行。在线编辑器适合验证小片段,本地环境适合写完整项目。无论哪种方式,代码本身是一样的。

操作步骤

第一步:创建数组

NumPy 的核心对象是 ndarray,通常通过 np.array() 从 Python 列表构造出来:

import numpy as np

arr = np.array([1, 2, 3, 4, 5])
print(arr)
print(type(arr))

输出会显示数组内容和它的类型 numpy.ndarray。传入嵌套列表就得到多维数组:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d)
print(arr2d.ndim)   # 维度数
print(arr2d.shape)  # 各维度长度

除了从列表转换,NumPy 还提供一批直接生成数组的函数,写测试数据和占位数据时很常用:

  • np.zeros((2, 3)):生成全 0 数组,参数是形状元组。
  • np.ones((2, 3)):生成全 1 数组。
  • np.arange(0, 10, 2):按起点、终点、步长生成等差序列,终点不包含在内。
  • np.linspace(0, 1, 5):在起点和终点之间均匀取指定个数的点,终点包含在内。

arange 和 linspace 的区别值得记一下:前者按步长切,后者按个数切。需要“每隔多少取一个”用 arange,需要“一共取几个”用 linspace。

第二步:索引取值

一维数组的索引和 Python 列表一致,下标从 0 开始,也支持负数从末尾倒数:

arr = np.array([10, 20, 30, 40, 50])
print(arr[0])    # 10
print(arr[-1])   # 50

多维数组用逗号分隔各维下标,而不是像列表那样写多层方括号:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d[0, 1])   # 第 0 行第 1 列,结果 2
print(arr2d[1, 2])   # 第 1 行第 2 列,结果 6

这种写法比 arr2d[0][1] 更符合 NumPy 的习惯,在维度更多时差别更明显。

第三步:切片

切片语法是 起始:结束:步长,结束位置不包含在内。一维切片:

arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(arr[2:5])    # [2 3 4]
print(arr[:4])     # [0 1 2 3]
print(arr[6:])     # [6 7 8 9]
print(arr[::2])    # [0 2 4 6 8]
print(arr[::-1])   # 反转

多维数组可以对每一维分别切片,用逗号隔开:

arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr2d[0:2, 1:3])

上面这段取的是前两行、后两列组成的子数组。只写一个冒号表示该维全取,例如 arr2d[:, 0] 取第一列。

第四步:数据类型

NumPy 数组里所有元素必须是同一类型,这一点和 Python 列表不同。创建时 NumPy 会根据输入自动推断类型,也可以用 dtype 参数显式指定:

a = np.array([1, 2, 3])
print(a.dtype)          # 整数类型

b = np.array([1.5, 2.5])
print(b.dtype)          # 浮点类型

c = np.array([1, 2, 3], dtype='float64')
print(c.dtype)

已有数组可以用 astype() 转换类型,它会返回一个新数组:

d = a.astype('float64')
print(d, d.dtype)

把浮点转成整数时小数部分会被截断而不是四舍五入,这一点在数据清洗时容易踩坑,转换前先想清楚是否需要先做取整处理。

第五步:副本与视图

这是 NumPy 里最容易出错的概念之一。切片得到的是原数组的视图,它和原数组共享同一块数据;修改视图会影响原数组。而 copy() 得到的是独立的副本,互不影响。

arr = np.array([1, 2, 3, 4, 5])
view = arr[1:4]
view[0] = 99
print(arr)   # 原数组被改动

arr2 = np.array([1, 2, 3, 4, 5])
copy = arr2[1:4].copy()
copy[0] = 99
print(arr2)  # 原数组不变

判断一个结果是视图还是副本,可以看它的 base 属性:视图的 base 指向原数组,副本的 base 是 None。写数据处理流程时,如果后续要修改切片结果又不想污染源数据,记得显式调用 copy()。

第六步:查看与改变形状

shape 返回各维长度组成的元组,ndim 返回维度数,size 返回元素总数:

arr = np.array([[1, 2, 3], [4, 5, 6]])
print(arr.shape)   # (2, 3)
print(arr.ndim)    # 2
print(arr.size)    # 6

改变形状用 reshape(),元素总数必须能整除目标形状,否则报错:

arr = np.array([1, 2, 3, 4, 5, 6])
print(arr.reshape(2, 3))
print(arr.reshape(3, 2))

形状参数里可以有一个位置写 -1,表示让 NumPy 自己推算这一维的长度:

print(arr.reshape(2, -1))   # 自动算出第二维为 3

如果只是想把多维数组拉平成一维,可以用 flatten() 或 ravel()。两者结果看起来一样,但 flatten() 返回副本,ravel() 尽量返回视图,对内存敏感的场景要注意这个差别。

第七步:遍历数组

一维数组可以直接用 for 循环遍历元素。多维数组默认按第一维遍历,每次拿到的是下一维的子数组:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
for row in arr2d:
    print(row)

要逐个访问所有标量元素,用 nditer:

for x in np.nditer(arr2d):
    print(x)

不过在实际计算里,更推荐直接用向量化运算代替显式循环,既短又快。

第八步:拼接与拆分

拼接用 concatenate,通过 axis 指定沿哪一维连接:

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(np.concatenate((a, b), axis=0))   # 沿行方向堆叠
print(np.concatenate((a, b), axis=1))   # 沿列方向拼接

拆分用 split,第二个参数是拆成几份:

arr = np.array([1, 2, 3, 4, 5, 6])
print(np.split(arr, 3))

如果元素个数不能均分,split 会报错,这时可以改用 array_split,它允许不等分。

第九步:查找、排序与筛选

查找某个值出现的位置用 where:

arr = np.array([1, 3, 5, 3, 7])
print(np.where(arr == 3))

排序用 sort,它返回排好序的新数组,原数组不变:

arr = np.array([3, 1, 4, 1, 5])
print(np.sort(arr))

筛选用布尔索引,把条件表达式直接放进方括号:

arr = np.array([1, 2, 3, 4, 5, 6])
print(arr[arr > 3])

布尔索引返回的是满足条件的元素组成的新数组,这是 NumPy 里最常用的数据筛选方式,比写循环加 if 简洁得多。

一个完整示例

下面这段代码把前面几步串起来:生成一组数据、改变形状、切片取值、筛选、拼接,最后打印结果。可以直接整段运行。

import numpy as np

# 1. 生成 0 到 11 的整数,并改成 3 行 4 列
arr = np.arange(12).reshape(3, 4)
print("原始数组:")
print(arr)

# 2. 查看形状与元素总数
print("形状:", arr.shape)
print("元素总数:", arr.size)

# 3. 取第 1 行
print("第 1 行:", arr[1])

# 4. 取第 2 列
print("第 2 列:", arr[:, 1])

# 5. 取左上角 2x2 子数组,并复制一份再修改
sub = arr[0:2, 0:2].copy()
sub[0, 0] = 100
print("子数组副本:")
print(sub)
print("原数组未被修改:")
print(arr)

# 6. 筛选出大于 5 的元素
print("大于 5 的元素:", arr[arr > 5])

# 7. 把数组拉平并转成浮点类型
flat = arr.reshape(-1).astype('float64')
print("拉平并转浮点:", flat)

# 8. 与另一个数组拼接
other = np.ones((3, 1))
joined = np.concatenate((arr, other), axis=1)
print("拼接后的数组:")
print(joined)

运行后你会看到:原始数组是 3 行 4 列;第 1 行是 [4 5 6 7];第 2 列是 [1 5 9];子数组副本被改成 100 后原数组保持不变;筛选结果只保留大于 5 的数;拉平后是一维浮点数组;拼接后多出一列全 1。

这个例子覆盖了数组创建、形状变换、索引、切片、副本、筛选、类型转换、拼接八类操作。把其中任意一步的输入换掉,就能验证自己对参数含义的理解是否正确,比如把 axis=1 改成 axis=0 看拼接结果怎么变。

注意事项

  • 切片是视图,不是副本。 修改切片结果会连带修改原数组。需要独立数据时显式调用 copy(),这是新手最常见的错误来源。
  • 数组元素类型必须一致。 往整数数组里塞浮点数,NumPy 会按规则统一类型,可能发生精度损失。构造数组时用 dtype 明确指定类型更稳妥。
  • reshape 要求元素总数匹配。 目标形状各维长度相乘必须等于原数组元素总数,否则抛出错误。用 -1 可以让 NumPy 推算其中一维,但只能有一个 -1。
  • arange 不包含终点,linspace 包含终点。 两者语义不同,混用会导致取到的数据范围差一个点。
  • split 要求能均分。 元素个数不能被份数整除时会报错,需要不等分时改用 array_split。
  • 浮点转整数是截断。 astype 把浮点转成整数时直接丢弃小数部分,不是四舍五入。需要取整时先单独处理。
  • 多维索引用逗号,不要写多层方括号。 arr[0, 1] 是 NumPy 的写法,arr[0][1] 虽然有时也能跑通,但在切片和高级索引组合时行为可能不同。
  • 版本与接口变动以官网为准。 NumPy 各版本之间会有接口新增与废弃,本文示例只演示基础用法,涉及具体版本行为、安装方式支持范围时,请查阅 NumPy 官网当前信息。

把上面九步按顺序跑一遍,再动手改改参数看输出变化,基本就能覆盖日常处理数组时最常遇到的操作。接下来可以继续往随机数生成、统计函数、线性代数这些方向扩展,它们都建立在同一套数组对象之上。