1. 先准备一个能跑的环境安装、导入与常见报错辨别提到 Numpy 数组的基本运算很多人都默认“这东西装上就能用”但我在实际带新手的过程中发现卡在第一关的人数远比想象中多。热搜词里“numpy安装”“numpy库”“python安装numpy库的方法”“userwarning: failed to initialize numpy: no module named numpy”这些词频繁出现说明大量同学根本不是被运算难住的而是被环境问题拦在了门外。先说安装。大多数情况下一行命令就能搞定pip install numpy如果是在国内网络环境速度往往会卡到怀疑人生建议直接加镜像源pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple用 Anaconda 的朋友更简单conda 环境里通常自带 numpy即使没带执行conda install numpy也很快。装完之后先别急着敲代码建议跑一个小验证import numpy as np print(np.__version__)这里顺便解释一下为什么我们约定俗成用import numpy as np。numpy 是一个模块名而几乎所有的官方文档、开源项目、技术博客都用np作为别名你跟着这个习惯走后面抄代码时能少踩很多坑。如果输出结果是一个类似1.26.4的版本号说明环境已经通了。不过这里要注意很多人明明刚装完 numpy运行程序却报ModuleNotFoundError: No module named numpy。我帮人排查这类问题无数次90% 的原因是当前正在运行的 Python 解释器和 pip 安装时使用的 Python 解释器不是同一个。比如你在终端里用python命令运行某个文件但这个终端的环境变量指向的是系统自带的 Python 3.8而你用pip install numpy装到的是另一个 Anaconda 环境里的 Python 3.11。如果你遇到类似问题不要急着重装先一步一步看下面这张表怀疑点验证命令判断方式当前解释器路径import sys; print(sys.executable)看这个路径和你装包的路径是否一致numpy 是否装在这个解释器里python -c import numpy; print(numpy.__version__)能打印版本号就是装上了是否意外用了多个环境conda env list或pip -V在哪个环境执行就把包装到哪个环境除了ModuleNotFoundError还有一类很迷惑的报错叫UserWarning: Failed to initialize NumPy: No module named numpy。这种通常不是你当前代码的问题而是某个第三方库在你的系统 Python 目录里做了 import 动作但那个目录没装 numpy。遇到这个提示优先检查你的 IDE 是否选对了解释器尤其是 VS Code、PyCharm 这类编辑器它们经常默认选中一个和你终端不一样的 Python 解释器。把解释器切换到装了 numpy 的那个环境问题通常就消失了。2. 创建数组是全部运算的前提从列表到 array、zeros、arange数组运算是建立在“你有数组”这件事之上的所以创建数组的方法反而成了基本运算里最先要解决的环节。很多从 C 或 Java 转过来的同学习惯了用数组下标一个个访问元素到了 Python 里又习惯写for循环去遍历列表。这种思维不能说错但完全没有发挥 numpy 的优势而且跑大数据量时效率非常难看。numpy 数组的核心概念是 ndarrayN-dimensional array多维数组你可以把它理解成一块连续的内存里面存的是同一类型的元素。对比 Python 原生列表最直观的差异有两点一是列表可以装不同类型的对象而 ndarray 的元素类型必须统一二是列表的操作大多要写循环而 ndarray 支持整个数组一起运算。先从最朴素的创建方式开始。把一个普通列表转成数组import numpy as np a np.array([1, 2, 3, 4, 5]) print(a, type(a)) # [1 2 3 4 5] class numpy.ndarray这段代码看起来没什么特别的但它已经完成了一次“类型统一”的转换。如果列表里既有整数又有浮点数numpy 会把所有元素都转成浮点数b np.array([1, 2.5, 3]) print(b.dtype) # float64这个细节非常关键。数组的dtype决定了后续运算时元素的精度和内存占用很多时候结果不对、速度慢根源都在 dtype 上。实际工作中我更推荐你显式指定 dtype尤其是从文件读取数据时c np.array([1, 2, 3], dtypenp.float64) print(c.dtype) # float64除了从已有数据创建numpy 还提供了一批生成“特殊数组”的函数实际场景里几乎每天都在用我常用的有这几个zeros np.zeros((2, 3)) # 2行3列的全0数组 ones np.ones((2, 3)) # 2行3列的全1数组 full np.full((2, 3), 7) # 2行3列全部填充7 arange_arr np.arange(0, 10, 2) # 类似range生成0,2,4,6,8 linspace_arr np.linspace(0, 1, 5) # 从0到1均匀取5个数这些函数解决的问题各不相同。np.zeros和np.ones通常用于“先占个固定形状的坑后面再填数据”的场景比如初始化权重矩阵np.arange则适合生成等差数列np.linspace在画图、采样时是神器它确保首尾两个点一定落在区间端点上而np.arange是加步长累加端点不一定包含。关于数组形状新手最容易晕的就是维度。一维数组是向量二维数组是矩阵三维以上可以理解成多个矩阵堆叠。判断形状靠shapearr np.zeros((2, 3, 4)) print(arr.shape) # (2, 3, 4) print(arr.ndim) # 3这里给从 C 转过来的同学提个醒在 C 里你写int a[2][3]访问方式是a[i][j]而 numpy 里是arr[i, j]虽然也支持arr[i][j]但前者才是 numpy 的推荐写法尤其是后面做切片和高级索引时逗号分隔远比连续下标清晰。创建数组时还有一个常见误解有人以为 Python 原生列表就是数组于是直接拿[1, 2, 3]去和其他数组做运算。这种做法偶尔能出结果但一旦涉及多维数据或者想调用.sum()、.reshape()这类方法时列表就会立刻报错。所以我的建议是明确区分“列表”和“数组”凡是需要批量数学运算的数据第一步先转成 ndarray。3. 逐元素运算加减乘除、比较与布尔运算既然环境到位、数组也建好了接下来就是标题的核心基本运算。numpy 数组最迷人的一点就是把“批量”两个字压缩成了符号。你不需要写循环直接对整个数组做算术它会自动对每一个元素执行相同的操作这就是逐元素element-wise运算。3.1 算术运算运算符形式和函数形式最简单的加减乘除直接用运算符就行import numpy as np a np.array([1, 2, 3, 4]) b np.array([10, 20, 30, 40]) print(a b) # [11 22 33 44] print(a - b) # [ -9 -18 -27 -36] print(a * b) # [10 40 90 160] print(a / b) # [0.1 0.1 0.1 0.1]注意这里的a * b不是矩阵乘法而是对应位置元素相乘。如果你学过线性代数想在 numpy 里做真正的矩阵乘法得用a b或者np.dot(a, b)。这一点对刚接触 numpy 的人来说很容易踩坑尤其是从 MATLAB 转过来的人MATLAB 里的*就是矩阵乘法而 numpy 里*是逐元素乘法。除了 - * /还有取整除法和取余a np.array([7, 8, 9]) b np.array([2, 3, 4]) print(a // b) # [3 2 2] print(a % b) # [1 2 1] print(a ** 2) # [49 64 81]每一个运算符也都有对应的函数形式np.add、np.subtract、np.multiply、np.divide、np.mod、np.power。如果你只是在脚本里算数据运算符写法更简洁如果你写的是需要动态传参的工具函数函数形式会更灵活。3.2 数组与标量的运算数组除了和数组运算还经常跟一个标量单个数字运算arr np.array([1, 2, 3, 4]) print(arr 10) # [11 12 13 14] print(arr * 2) # [ 2 4 6 8] print(arr / 2) # [0.5 1. 1.5 2. ]这就是广播的第一个案例关于广播的完整规则我会在下一节展开。这里先记住一个直觉标量和数组运算时相当于这个标量被“复制”到了数组的每个位置然后逐一运算。3.3 比较运算返回布尔数组比较运算符!也支持逐元素运算结果是一个由 True 和 False 组成的布尔数组arr np.array([1, 5, 3, 8, 2]) print(arr 3) # [False True False True False] print(arr 5) # [False True False False False] print((arr 2) (arr 5)) # [False True True False True]这里有个非常经典的坑在 C 或 Java 里你写if (arr 5)是在判断整个数组是否等于某个值但在 numpy 里arr 5得到的是一整个布尔数组不能直接丢进if。如果你想把“数组中是否包含某个值”当条件用正确写法是if (arr 5).any(): print(存在等于5的元素)同理判断两个数组是否完全相同别用去和 if 配合应该用np.array_equal(a, b)它返回一个真正的布尔值。3.4 布尔索引把比较结果变成筛选条件布尔数组最实用的场景是当作“掩码”来筛选数组元素这就是布尔索引arr np.array([10, 25, 3, 47, 8]) mask arr 10 print(arr[mask]) # [25 47]甚至可以直接把筛选和赋值结合满足条件的元素会被替换arr[arr 10] 0 print(arr) # [10 0 3 0 8]这个操作在数据清洗里极其常用比如把异常值置零、把超限数据替换成边界值。逻辑组合时要注意Python 的and/or不能直接用要用位运算符和|arr np.array([1, 6, 4, 9, 3]) print((arr 2) (arr 8)) # [False True True False True]顺便提一个新手容易困惑的点为什么不能用and因为and语法要求两边的表达式都能转换成“一个”布尔值而数组没法这样转换运行时会直接报ValueError: The truth value of an array with more than one element is ambiguous。此时才是逐元素逻辑运算的正确写法。4. 广播机制形状不一致时数组运算的展开规则上一节提到标量直接和数组运算其实背后已经在用广播。很多人在学 numpy 数组基本运算的时候把注意力都放在加减乘除的符号上觉得广播机制是进阶内容可以先放一放。但根据我的经验跳过广播直接往下学是不太行的因为实际数据几乎不可能都长成完全相同的形状你总会遇到一个二维数组加一个一维数组的情况不懂广播就会卡死在那里。4.1 广播的直观理解广播broadcasting的意思就是当两个数组的形状不完全相同时numpy 会自动扩展较小数组的维度让它们能够逐元素对齐然后继续运算。我们看一个最典型的例子a np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3) b np.array([10, 20, 30]) # 形状 (3,) result a b print(result) # [[11 22 33] # [14 25 36]]这里b明明是一维数组长度是3而a是二维数组形状是 (2, 3)。按常理它们不能相加但广播规则允许 numpy 把b“沿着行方向复制一遍”变成形状 (2, 3) 的数组再和a逐元素相加。这个“复制”并不是真的在内存里多复制一份数据而是运算时虚拟广播所以效率很高。4.2 广播的具体规则广播的判断并不是靠猜的它有一套严格的后对齐规则我每次讲给新手都会反复强调这句话把两个数组的形状从右往左逐个维度对齐每个维度上要么相等要么其中一个等于1要么某个维度缺失。按照这个规则分两步判断第一步从尾部最后一个维度开始比较。形状 (2, 3) 和 (3,) 对齐时b缺少第一个维度可以理解为它的形状自动补成 (1, 3)。第二步比较对应维度。第0维度2 vs 1一个是1可以扩展成2第1维度3 vs 3相等直接通过。于是最终结果是形状 (2, 3)。再举一个稍微复杂的例子列向量加行向量。col np.array([[1], [2], [3]]) # 形状 (3, 1) row np.array([10, 20, 30]) # 形状 (3,) result col row print(result.shape) # (3, 3) print(result) # [[11 21 31] # [12 22 32] # [13 23 33]]col的形状是 (3, 1)row的形状补为 (1, 3)逐维度看3 vs 1 能广播1 vs 3 也能广播最终得到 (3, 3)。这种操作在计算“每个元素都偏移一个向量”的场景里很常见比如给一整个样本矩阵的每一行都加上同一个偏置。如果两个维度既不相等又不含1那么广播失败numpy 会报出很经典的错误a np.ones((2, 3)) b np.ones((4,)) a b # ValueError: operands could not be broadcast together with shapes (2,3) (4,)看到这个报错第一反应不要慌把两个 shape 并排写出来(2,3) 和 (4,)尾部维度 3 vs 4不相等也不含1所以失败。解决办法通常是调整形状或者把breshape 成能广播的形状。4.3 广播实战的思维方式从我个人经验来说掌握广播最好的方式不是死记规则而是“用规则去怀疑每一个计算结果”。比如你想把某个数组中所有元素减去该数组每行的均值如果用纯 Python 写要套两层循环但用广播只需要两行data np.array([[80, 90, 85], [70, 75, 72], [88, 92, 96]]) row_mean data.mean(axis1, keepdimsTrue) # 保持形状为 (3,1) centered data - row_mean print(centered)关键就在keepdimsTrue。如果不加这个参数row_mean的形状是 (3,)而 data 是 (3, 3)尾部维度 3 vs 3 会变成将每个维度减去对应的标量结果是每个元素都减掉了不同位置的均值整体含义就错了。加了keepdimsTrue之后形状是 (3, 1)广播规则会把它沿横方向扩展成每一行减去对应行的均值结果才是我们真正想要的“去中心化”。关于保持维度这一点我见过太多人吃亏。永远记住一句话在用广播处理行方向或列方向的统计量时优先考虑把结果保持为二维形状否则很容易得到“看似能跑、但结果全错”的代码。5. 用 axis 理解聚合、变形与排序从求和到读取成绩表基本运算讲完之后还有一个必须补上的维度就是数组的聚合操作。因为在实际使用中很少有人真的只做一次加法或乘法你往往需要对整个数组求总、求均值、找最大最小值或者调整数组形状让它能和其他数据对齐。这类操作不复杂但 axis 的概念是绕不开的坎。5.1 形状变换reshape、flatten 与转置先看形状变换。reshape是出现频率最高的一个工具它可以在元素总数不变的前提下把数组改成任意形状arr np.arange(12) matrix arr.reshape(3, 4) print(matrix) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]]reshape(-1, 4)是一种很常见的写法这里的-1表示“让 numpy 自己推导这一维的大小”。比如已知列数是4元素总数是12那么行数自动就是3。这个写法在读取数据、构造特征矩阵时特别实用省得自己数元素个数。flatten则是把任意多维数组拉平成一维print(matrix.flatten()) # [ 0 1 2 3 4 5 6 7 8 9 10 11]转置T用来交换维度位置对二维数组就是行列互换print(matrix.T) # [[ 0 4 8] # [ 1 5 9] # [ 2 6 10] # [ 3 7 11]]这里有一个小提醒reshape和T返回的是原数组的“视图”还是“副本”取决于具体条件reshape大部分时候是视图而flatten返回的一定是副本。如果你不确定修改变换后的数组会不会影响原数组最稳妥的办法是先.copy()一下。5.2 axis 参数到底在描述什么聚合操作比如sum、mean、min、max、std都有一个高频参数axis。很多人背公式axis0是列方向axis1是行方向。可是方向这个词太容易记乱了我建议换一种理解方式axis 的值表示你沿着“哪个轴”移动求和最终这个维度会被“压缩”掉。或者更直白地说axis0表示对每一列操作结果是每一列的汇总值axis1表示对每一行操作结果是每一行的汇总值。用一个学生成绩表的例子来解释。假设数据是一个二维数组每一行是一个学生每一列是一门科目scores np.array([ [80, 90, 85], [70, 75, 72], [88, 92, 96], [60, 65, 70], ])如果我想算每门科目在所有学生里的平均分也就是求每一列的平均值subject_mean scores.mean(axis0) print(subject_mean) # [74.5 80.5 80.75]因为列是科目axis0把第一维学生的数量压缩掉了剩下的是每门课的成绩。如果我想算每个学生的平均分也就是每行的平均值student_mean scores.mean(axis1) print(student_mean) # [85. 72.33333333 92. 65. ]这里axis1把第二维科目的数量压缩掉了剩下的是每个学生的均值。我每次带人做这类统计时都会画一张简单的示意图本质上就是“沿着 axis 的方向滑动最后去掉的是那个方向上的长度”。大家以后自己写代码也别死记 axis0 或 axis1直接在心里问一句我要对哪一维的数据做聚合把那一维放到 axis 上基本不会错。5.3 排序直接排序与索引排序聚合之后经常伴随排序。numpy 里最常见的两个排序函数是sort和argsort。sort直接返回排好序的新数组arr np.array([3, 1, 4, 1, 5, 9, 2, 6]) print(np.sort(arr)) # [1 1 2 3 4 5 6 9]argsort则更有意思一点它返回的是排序后的索引数组而不是值本身idx np.argsort(arr) print(idx) # [1 3 6 0 2 7 4 5] # 表示 arr[1]1, arr[3]1, arr[6]2, ... 按这个顺序取就能得到排好序的数组索引排序在实际工作中非常有用。比如你想对二维数组按第一列排序就必须借助argsort拿到行的顺序再按这个顺序重新排列整个矩阵data np.array([[3, 100], [1, 200], [2, 150]]) row_idx np.argsort(data[:, 0]) # 按第一列值排序 sorted_data data[row_idx] print(sorted_data) # [[ 1 200] # [ 2 150] # [ 3 100]]5.4 组装一个小例子输入10个学生的成绩并求平均分把上面这些知识点组合起来就能解决热搜词里出现频率极高的一个问题“输入10个学生的成绩求出平均分用数组”。import numpy as np # 假设每个学生的成绩用一行表示一共4门科目 scores np.array([ [82, 93, 78, 90], [67, 72, 88, 75], # ... 一共10行 ]) # 每个学生的平均分结果是一维长度10的数组 student_avg scores.mean(axis1) # 每门科目的平均分结果是一维长度4的数组 subject_avg scores.mean(axis0) print(每个学生的平均分:, student_avg) print(每门科目的平均分:, subject_avg)如果要处理用户输入可以用np.array([...])的方式收集数据或者用np.fromiter从迭代器直接创建数组。总之理解 axis 和聚合之间的关系这些统计问题就是一行代码的事完全不需要写显式循环。6. 我把这些坑都踩过安装、类型、广播与常见报错修复最后这一节我把这几年在 numpy 基本运算上踩过的、替别人排过的坑集中列出来。每一项都是真实报错每一条都能在热搜词里找到对应。6.1 AttributeError: module numpy has no attribute float这个报错在热搜词里排在很靠前的位置说明确实有很多人遇到。原因并不复杂numpy 1.20 版本开始删除了一堆原本从 Python 内置类型映射过来的别名比如np.float、np.int、np.bool等。如果你看的是比较老的教程里面写了np.float在新版本里运行就会直接报这个错。解决办法是改成 Python 原生的类型# 错误写法 x np.array([1, 2, 3], dtypenp.float) # 正确写法 x np.array([1, 2, 3], dtypenp.float64) # 或者直接用内置类型 y np.array([1, 2, 3], dtypefloat)类似地np.int改成int或np.int64np.bool改成bool或np.bool_。6.2 整数数组的除法陷阱与精度问题看一个很容易被忽略的例子a np.array([5, 7, 9]) b np.array([2, 3, 4]) print(a / b) # [2.5 2.33333333 2.25 ]在 Python 3 里/本身就是真除法所以 numpy 里也是浮点结果。但如果你用//做整除结果是整数print(a // b) # [2 2 2]如果你是 C 转过来的写惯了整数除以整数的截断行为到了 numpy 里一旦用错除号精度差别会非常明显。另一个隐藏的坑是两个整数数组做a / b得到浮点数组但如果你把这个结果再塞回一个整数数组小数部分会被直接截断且 numpy 可能不会给你任何警告。6.3 比较两个数组是否相等别用 if arr1 arr2前面已经提过一次这里再强调判断两个数组元素是否完全相等用np.array_equal(a, b)。如果你写a b得到的是一个布尔数组如果你把它塞进if里一定报ValueError。要判断“是否存在相等元素”用(a b).any()要判断“所有元素是否都相等”用(a b).all()。6.4 广播报错operands could not be broadcast together这个报错前面的章节里分析过核心解决思路就是看 shape。我遇到有人写代码不管形状直接拿两个完全不相干的数组去加然后被报错搞懵。这里提供一个实用的排错顺序打印两个数组的.shape并排写下。从右往左逐位比较。找到既不相等又不为1的位置那就是问题的根源。判断是否需要reshape、转置、或者给统计结果加keepdimsTrue。6.5 就地运算和复制 与 的区别a a 1和a 1在 numpy 里效果看起来一样但底层有一个细微差别前者会把原来的数组复制一份再赋值给a很可能改变a在内存里的引用后者是就地在原数组上修改。如果你有另一个变量同时指向同一个数组用时那个变量也会跟着变有时这是你想要的有时它会带来隐蔽的 bug。同理arr.copy()是显式创建一份独立副本。当你需要对数组切片做修改但又不想改变原数组时一定要记得先复制。举个例子arr np.arange(10) view arr[:5] view[0] 99 print(arr[0]) # 99因为切片是视图view改的是同一块内存如果你希望view独立于arr就要用arr[:5].copy()。6.6 环境类问题的最终排查思路如果你在运行 numpy 程序时遇到ModuleNotFoundError: No module named numpyUserWarning: Failed to initialize NumPy: No module named numpy优先检查解释器路径而不是重新安装。具体方法前面提到过用import sys; print(sys.executable)看当前解释器位置再在终端里跑pip show numpy看安装位置两个地方对不上就切换 IDE 的解释器或者激活正确的虚拟环境。这个排查思路适用所有 Python 第三方库不只是 numpy。就我个人习惯每次跑新环境里的 numpy 脚本都会先跑一遍“五连验证”import numpy as np print(np.__version__) print(np.zeros((2, 2))) print(np.array([1, 2, 3]) np.array([4, 5, 6])) print(np.arange(12).reshape(3, 4).mean(axis0))只要这四行没有任何报错接下来不管写什么数组运算环境这一关都算过了。剩下的问题就都能靠看 shape、查 dtype、想清楚 axis 来解决。这些才是 numpy 基本运算真正需要内化的核心思路。