简介:面向NumPy初学者与数据分析入门者,这份代码包以Python语言系统讲解NumPy核心用法,涵盖数组创建、属性查看、数据类型转换、索引与切片、广播机制、数组排序以及reshape/transpose等关键操作。压缩包共147个文件,其中123个为Python脚本,另包含readme说明文档、CSV数据样本、MAT数据文件和少量图片素材,整体仅65KB,体积轻巧,便于本地运行与反复练习。目前已有389人学习下载,资源内置AAPL、BHP等真实行情数据,帮助读者在贴近实际的场景中理解NumPy的批量计算与统计能力。所有示例由浅入深,配有清晰注释和输出对比,能让读者直观看到一维数组、矩阵运算、维度变换等运行结果,快速掌握ndarray的常用方法,为后续科学计算与机器学习打下扎实基础。
1. 为什么说 NumPy 学习指南代码是数据流的底层泵
很多人学 Python 只用了三五天,就敢拿纯列表推导式处理数据。可一旦把数据量从几百推到几百万,列表推导式立刻露出疲态;同样的求和逻辑套进 NumPy 往往只需要一句代码,耗时却降了一个数量级。这份 python-NumPy 学习指南代码要做的,就是把从「会写 Python」到「会用 ndarray 思考」之间的那段过渡铺成一条能动手复现的路。真正拉开差距的不是语法糖,而是数据存放方式和底层循环交给了谁。
认真读完并动手敲一遍,你会发现它其实覆盖三件事:怎么把数据装进 ndarray,怎么用索引、切片、广播和聚合把它改造成想要的形状,以及当结果偏离预期时,如何定位问题而不是靠猜。适合刚啃完 Python 语法、准备进入数据分析或科学计算的读者,也适合已经跑过不少脚本、但对视图/拷贝、dtype、广播规则总会踩坑的熟手。
我写这份指南时的原则只有一个:每个代码块你都能在本机原样跑通,每个结论都写明为什么。只读不敲,等于白看。
2. 先把环境立住:Python、虚拟环境与 NumPy 的最小安装路径
2.1 为什么每个科学计算项目都绕不开 ndarray:选型之前的三个理由
先说清楚为什么是 NumPy,而不是继续用 Python 原生 list。理由有三条,按重要程度排。
第一,连续内存。ndarray 里的数据在物理上是紧挨着的一块缓冲区,元素之间的地址间隔完全固定。CPU 读内存是按缓存行批量加载的,连续数据能让缓存命中率高得离谱;而 Python list 存的是指针数组,每个元素还要再去堆上找对象本体,一次遍历等于多次随机访问。数据量越大,这个差距越明显。
第二,dtype 显式声明。你在创建数组时就告诉 NumPy 这块内存里装的是 float64、int32 还是 uint8。类型一旦定死,后续所有运算都不需要像 Python 那样每次迭代都去解析对象类型。这也是为什么同样的加法,ndarray 的循环可以交给编译器优化,而 Python 的循环每个元素都要查一遍类型。
第三,向量化。NumPy 把逐元素运算封装成 ufunc,真正的循环发生在 C 层,还能自动利用现代 CPU 的 SIMD 指令做单指令多数据计算。你写a + b,它不是在 Python 里循环,而是在底层一次性处理一整块内存。
这三点合在一起,决定了 NumPy 不是「更快的 Python」,而是一台面向数值数据的泵。你的数据进来,它负责把形状、类型、运算路径全部捋顺,再快速吐出去。
2.2 从零到可运行:虚拟环境、pip 安装与验证脚本
我建议每个学习项目都用虚拟环境,别图省事往系统 Python 里装。用官方自带的 venv 就够了,不需要任何额外工具。
# 创建虚拟环境,名字随意,我习惯用 .numpy-guide python3 -m venv .numpy-guide # 激活环境 source .numpy-guide/bin/activate # 先升级 pip,避免老版本解析依赖出问题 python -m pip install --upgrade pip # 安装 NumPy python -m pip install numpy安装完成后别急着写代码,先跑一条最简验证,确认二进制包能正常加载、CPU 指令集没踩到兼容问题:
python -c "import numpy as np; a = np.arange(12); print(a.sum())"看到66就说明环境已经通了。这里np.arange(12)生成 0 到 11 的整数数组,.sum()做聚合求和。如果这条命令报错,九成是 pip 装到了别的 Python 版本上,先执行which python确认解释器路径在虚拟环境目录里。
提示:Windows 下激活命令是
.numpy-guide\Scripts\activate,macOS/Linux 则是source .numpy-guide/bin/activate。方向别搞反,这是新手最常见的第一道坎。
2.3 版本选择:Python 版本、NumPy 版本与常见兼容组合
NumPy 的版本策略属于「向后兼容为主、大版本揭瓦」。我在实际项目里一般按这张表来选,你直接照抄就行:
| 使用场景 | 建议组合 | 理由 |
|---|---|---|
| 纯入门学习 | Python 3.10 + NumPy 1.26.x | 资料最多,几乎所有旧教程都能直接跑 |
| 新开数据分析项目 | Python 3.12 + NumPy 2.x | 新 API 更规范,性能有提升 |
| 维护老代码 | 保持原环境不动 | 科学计算生态有大量 C 扩展,升级大版本可能牵扯依赖重建 |
装 NumPy 基本不需要手动编译。正常渠道会拉取预编译的 wheel 包,导入即用。如果你遇到 pip 在构建源码而非下载 wheel,多数情况是 pip 版本太老或 Python 版本不被当前 NumPy 支持。
另一个常见现象是:同一台机器上同时存在多个 Python,pip install numpy装到了 A 环境,而你的解释器跑的是 B 环境。虚拟环境能根治这个问题,所以别跳过 2.2 节的步骤。环境立不住,后面所有代码演示都是空中楼阁。
3. 用代码拆开 NumPy 的五块拼图:创建、索引、切片、广播与聚合
3.1 创建数组:从 list 到 zeros、arange、random,先把数据放进 ndarray
创建一个数组是 NumPy 学习的第一块拼图。最基础的方式是np.array,它把 Python 的嵌套列表转成 ndarray,并自动推断 dtype:
import numpy as np # 列表转数组,这里显式指定 float64,后面算均值才不会丢精度 a = np.array([1, 2, 3], dtype=np.float64) # 固定形状的填充数组 z = np.zeros((2, 3), dtype=np.int32) o = np.ones((4,)) f = np.full((2, 2), 7.0) # 按步长或点数生成 r1 = np.arange(0, 1, 0.2) # 浮点步长,注意边界 r2 = np.linspace(0, 1, 5) # 固定取 5 个点,均匀分布 # 随机数:推荐用 Generator,而不是旧版的 np.random.seed rng = np.random.default_rng(42) noise = rng.normal(0.0, 1.0, (3, 4)) # 均值 0、标准差 1 的正态噪声np.arange(0, 1, 0.2)在浮点精度下很容易少一个元素。0.2 的二进制表示本身不精确,加到最后可能停在 0.9999999 而非 1.0,所以结果是五个数而不是六个。需要明确数量时,用linspace这种按点数切分的函数更稳妥。随机数部分务必用default_rng,它比旧的np.random.seed+np.random.rand组合更可控,随机状态隔离得也更干净。
3.2 索引与切片:一维、二维与布尔索引的语义边界
索引和切片是改动数组最常见的手段,也是新手把数据搞乱的高发区:
a = np.arange(12).reshape(3, 4) # 轴 0 是行,轴 1 是列 row = a[1] # 取第 1 行,返回一维数组 col = a[:, 2] # 取第 2 列,同样是 1 维 block = a[1:, 1:3] # 行 1 到末尾、列 1 到 2 # 布尔索引:条件为 True 的位置会被保留 mask = a % 2 == 0 even = a[mask] print(row, col, block.shape, even.shape)注意a[1]选中的是行而非列,因为 NumPy 默认轴 0 在最外层。布尔索引是一个隐蔽的坑:a[mask]返回的even是一维数组,原始二维结构直接消失。如果后续还要按行处理,就得自己 reshape 或用np.where保持形状。索引结果到底是视图还是拷贝,我放在第 5 章讲,这里先记住一个现象:报错不可怕,可怕的是形状悄悄变了。
3.3 形状变换与轴的理解:reshape、transpose 和 flatten 到底改了什么
数据形状是 ndarray 的骨架,轴则是形状的解释方式:
a = np.arange(24).reshape(2, 3, 4) # 只改形状不改数据顺序:按 C 顺序逐行填充 b = a.reshape(4, 6) # 轴交换:把轴 0 和轴 1 对调,返回的是视图 t = a.transpose(1, 0, 2) # flatten 返回拷贝,ravel 能返回视图就返回视图 c = a.ravel()reshape只要求元素总数不变,(2, 3, 4)有 24 个元素,(4, 6)也有 24 个,所以能直接变。transpose的输入参数是新顺序下各轴的来源,(1, 0, 2)意味着新轴 0 来自原轴 1。新手常忽略的一点是transpose返回的是视图,数据内存没有复制,后续修改可能互相影响。ravel和flatten的差别更明显:flatten保证新数组独立,ravel可能共用内存,改其中一个另一个会跟着变。需要独立副本时,别省那个.copy()。
3.4 广播机制:形状不一致时的自动扩散规则
广播堪称 NumPy 最巧妙的机制,也是新手最头痛的规则:
x = np.array([[1], [2], [3]]) # 形状 (3, 1) y = np.array([10, 20, 30, 40]) # 形状 (4,) z = x + y # 自动扩展成 (3, 4) print(z.shape) # (3, 4)广播规则只有三条:从最后一个维度向前对齐;维度大小为 1 或缺失时自动扩展;两个维度既不相等又不是 1,则无法广播。x的形状是(3, 1),y是(4,),对齐后x的第二个维度是 1,于是被拉伸成 4;y相当于补成(1, 4),被纵向拉伸成 3。
而(3, 3)的数组加(2,)会直接抛错,因为尾对齐后 3 和 2 既不等也不含 1。调试广播问题时最有效的动作,就是打印两个数组的.shape,尾对齐逐位看。
3.5 聚合与向量化计算:sum、mean、where 与 ufunc 的分工
聚合计算把整块数据压成少量数值,是统计和机器学习的日常动作:
data = np.arange(1, 101, dtype=np.float64) # 全域统计 print(data.mean(), data.std(), data.min(), data.max()) # 沿轴聚合:axis 指明压缩哪个轴 m = data.reshape(10, 10) col_mean = m.mean(axis=0) # 每一列的均值,结果长度 10 row_mean = m.mean(axis=1) # 每一行的均值,结果长度 10 # where:三目运算的向量化版本,选择会保留原形状 clip = np.where(data > 90, 90, data)axis=0压缩的是行,所以结果长度等于列数;axis=1反之。这个方向很多人一开始记反,我的记忆法是:axis等于几,就表示把第几个轴压没。np.where在数据清洗里出镜率极高,它不改变输入数组,而是返回一份新结果,适合做截断、替换异常值这类操作。
4. 性能不是玄学:ndarray 与纯 Python 列表的差距到底在哪
4.1 一次公平的基准:timeit 下纯 Python 列表与 ndarray 的差距
口说无凭,先跑一次可复现的对比:
import numpy as np from timeit import timeit N = 10_000_000 def list_sum(): # range 本身就是惰性序列,sum 会在 Python 层逐个累加 return sum(range(N)) def np_sum(): a = np.arange(N) return a.sum() # number=3 表示每段代码执行 3 次后取平均,避免单次抖动 print(timeit(list_sum, number=3)) print(timeit(np_sum, number=3))我第一次跑这段代码时,纯 Python 那行用了数秒,NumPy 那行快到让人怀疑是不是没执行。一个数量级的差距是起步,数据更复杂时能差出两个数量级。这里比的是「创建 + 计算」的全过程,没有偏袒任何一方,因为真实脚本里数组本来就需要创建。
4.2 为什么快:连续内存、SIMD 与内层循环的退出
差距的根源不是某些人以为的「C 语言比 Python 快」这么一句话。更准确地说,是三层原因叠加:
连续内存让数据在物理上聚成一团,CPU 缓存一次能捞起一片;dtype 固定让每个元素大小一致,寻址变成简单的基址加偏移,不需要解引用指针;sum 的计算循环整个在 C 层执行,编译器可以放心做 SIMD 向量化,把多组数据一次性塞进寄存器。
Python 列表则不同。它每个位置都是 PyObject 指针,类型信息分散在各个对象头上,两个整数相加要调用三次特殊方法,还要处理引用计数。这条路走的人再多也快不起来。所以结论不是「Python 不行」,而是「用 Python 写逐元素循环,是拿错了工具」。
4.3 向量化不是语法糖:哪些循环该改写、哪些该留着
很多初学者把「向量化」理解成「所有 for 循环都是坏的」,这属于矫枉过正:
# 逐元素操作:一定要向量化 def add_one(a): return a + 1 # 带提前返回的搜索逻辑:Python 循环反而更直接 def first_ge(arr, limit): for v in arr: if v >= limit: return v return -1a + 1这种逐元素算术必须交给 ufunc;而 likefirst_ge这种找第一个满足条件的元素,NumPy 没有通用内置,向量化写法要么绕、要么引入不必要的临时数组,代码可读性也跟着崩。正确的判断标准是:操作是否天然作用在整块数据上。聚合、逐元素变换、条件筛选,这些都是向量化的主战场;依赖运行期 break 的控制流,用普通循环更稳。
4.4 在真实项目里 NumPy 负责什么:一张分工表
落到真实的数据分析或科学计算项目,NumPy 很少单独出现,但它承担的角色非常明确:
| 任务 | 常用 API | 该不该自己写循环 |
|---|---|---|
| 数据加载与清洗 | np.loadtxt/np.genfromtxt | 不要 |
| 逐元素数值变换 | 四则运算、np.sqrt、np.log | 不要 |
| 统计聚合 | mean/std/percentile | 不要 |
| 条件筛选替换 | np.where/ 布尔索引 | 布尔索引可以,循环不推荐 |
| 依赖前一步状态的递推 | Python 循环后整体转 ndarray | 建议保留 Python 循环 |
这份分工表是我做模拟项目 X 时的默认选择:能向量化的一定向量化,不能向量化的先写清晰循环,再用数据整体转 ndarray 收口。性能优化最后做,行为正确先保证。把五块拼图拼齐之后,你写的数据处理代码会同时具备快和可读两个特征。
5. 从编译错误到无声错值:我在 NumPy 上学到的五个避坑实录
5.1 广播失败:形状不一致到底错在哪一步
现象:np.arange(3)加上np.arange(4)直接抛ValueError,提示operands could not be broadcast together with shapes (3,) (4,)。
原因:广播从最后一个维度对齐,3 和 4 既不相等也不含 1,无法自动扩展。
解决:打印.shape逐位对齐;需要让数组按行或按列参与运算时,先手动补维度,例如a.reshape(-1, 1)或b[np.newaxis, :]。记住一句口诀:维度为 1 的轴才有资格被拉伸。没有维度的轴,NumPy 不会脑补。
5.2 切片改了原数组:视图与拷贝的经典翻车
现象:给a做个切片b = a[1:3],然后改b[0] = 999,回头一看a也变了。
原因:基础切片返回的是视图,共享同一块内存。基础索引(整数、切片、...)一般返回视图;高级索引(列表、布尔 mask)通常返回拷贝。
解决:需要独立副本时显式调用.copy()。我在写数据处理脚本时的习惯是:任何切片对象只要后续会被修改,一律先问一句「我要不要改原数组」。拿不准时执行print(np.shares_memory(a, b)),输出True就说明两者在同一块内存上。
5.3 dtype 陷阱:无符号整数减出 255,结果还没人报警
现象:np.array([0, 1, 2], dtype=np.uint8)里做a[0] - a[1],得到 255,而不是 -1。
原因:uint8是无符号 8 位整数,取值 0 到 255,减法下溢时会绕回最大值,而且 NumPy 默认不报溢出错误,无声无息地给你一个看似合理的数值。
解决:运算前显式提高 dtype。常见做法是a.astype(np.int16)或a.astype(np.float64),统计类运算尽量用浮点数组。特别是读图像像素或字节流数据时,几乎都要在第一步把 dtype 拉宽,否则后面所有统计都建立在错误数据上。
5.4 布尔索引的维度收缩:一不留神丢掉轴
现象:二维数组a做a[a > 0],你期望得到一个二维结果,实际拿到一维数组,形状从(m, n)变成(非零元素个数,)。
原因:布尔索引本身会把你筛选的元素拉平成一维序列,因为 True 的位置在逻辑上不再是规则的矩形网格。
解决:想保留形状就改用np.where(a > 0, a, 0);想沿某个轴筛选,就用np.any或np.all先把布尔条件缩成轴方向的一维 mask,再配合a[mask_row]之类的操作。聚合场景下,可以配合keepdims=True让结果的形状语义更清晰。
5.5 在循环里反复拼接 ndarray:内存暴涨
现象:在 for 循环里用np.concatenate不断累积结果,处理几十万行数据时内存直接翻车,进程被杀。
原因:concatenate每次都要申请一块新内存,把旧数据完整拷过去,再释放旧缓冲区。循环 N 次,总拷贝量是 O(N²),GC 跟不上就爆了。
解决:收集阶段用普通 Python list,循环结束后一次性np.concatenate(list_of_arrays)。或者干脆预分配一块足够大的输出数组,每次用切片写入,不产生中间拷贝。这条血泪经验几乎每个写过批量处理脚本的人都遇到过,提前规避,省一整晚调试时间。
6. 让学习指南长出肌肉:用测试断言固定你对代码的理解
6.1 把目标函数先写出来,再给自己上断言
看完这份指南,你会写不少小函数。但如果只跑一次、看一眼输出就关掉,理解是留不住的。我习惯给每个关键函数补上断言:
import numpy as np def zscore(x): # 对每组数据做标准化:减去均值,除以标准差 mu = x.mean() sd = x.std() return (x - mu) / sd # 断言正确的参考值,atol 表示绝对误差容忍范围 np.testing.assert_allclose( zscore(np.array([1.0, 2.0, 3.0])), np.array([-1.0, 0.0, 1.0]), atol=1e-12, ) print("zscore 通过验证")assert_allclose是浮点比较的正确打开方式。直接用==比浮点数,你会被精度抖动反复教育。atol控制绝对容差,rtol控制相对容差;普通计算用atol=1e-12足够,工程场景放宽到1e-8也很常见。每次改完函数就重新跑断言,比盯着打印结果猜要可靠得多。
6.2 三个值得固化的小习惯
第一,拿到不确定的数组先打印.shape和.dtype,两个属性就能回答大部分「结果为什么这样」的问题。第二,修改任何切片前跑一句np.shares_memory,确认有没有牵连原数组。第三,把本指南里的示例改造成你自己的小工具函数,比如数据截断、按列标准化、构造随机训练集,每个都配上断言。
我现在拿到任何一批新数据,都会先写下三行验证脚本:导入、断言、基准。行为固定了才谈性能优化。这份 NumPy 学习指南代码里最值钱的不是某个 API,而是「先写断言再动手改」的习惯。踩过的坑都是资源,希望帮到你。
本文还有配套的精品资源,点击获取