数组里每个元素占一样多的字节,类型统一是它省内存的根本。 astype 返回新数组,不接住返回值等于没改。 星号是逐元素乘,矩阵乘要写 @。这一处搞错,结果完全是两回事。
上一篇讲怎么造数组。这一篇讲数组里装的是什么,以及能对它做什么运算。 本机 numpy 2.5.2、python 3.13.14,耗时统一预热后取最快。
一、三个属性看清一个数组
A.dtype 是类型,A.itemsize 是每个元素占几字节,A.nbytes 是整块占多少。
np.array([1, 2, 3]) 的 dtype 是 int64,itemsize 8,nbytes 24。
三者是乘出来的关系:24 等于 8 乘 3。不是三个互相独立的数。
int64 的意思是每个整数占 64 位,也就是 8 个字节,能存的范围很大。 小一号的 int8 只占 1 个字节,范围是 -128 到 127。
二、装不下的时候,新版直接报错
往 int8 里塞 200,numpy 2.5 直接抛 OverflowError。 因为 200 超出了 int8 的上限 127,它装不下。 老版本不报错,会静默溢出成 -56。200 减 256 等于 -56,绕回去了。
报错顶多让程序停下来,静默溢出是让错的那个数继续往下算。 后者危险得多。等你发现结果不对,已经不知道是哪个环节转错的。 所以读数据之后先看 dtype,别等到算出一堆怪数才回头查。
三、astype:换类型要接住返回值
A.astype(float) 把整数转成浮点,打出来每个数后面多一个小数点。
但它返回的是新数组,原数组一个字都没动。
必须写 A = A.astype(float)。只写前半句,跑了也白跑。
这条和 Pandas 的 astype、replace、dropna 是一套规矩:默认不原地改。
判断标准很简单:函数有没有返回东西。返回了,就得接住。
四、逐元素运算,还有快 55 倍的向量化
A 是 [1, 2, 3],B 是 [10, 20, 30]。
A + B 给 [11 22 33],A * B 给 [10 40 90],B / A 给 [10. 10. 10.]。
位置对位置,一个算一个。除法结果自动变成浮点,不会截成整数。
A * B 是逐元素乘,不是矩阵乘。矩阵乘要写 A @ B,下一篇专门讲。
100 万个数乘 2:列表推导 118.8 毫秒,数组 2.1 毫秒,快 55 倍。
数组不用写循环,一行乘法就整块算完。这叫向量化。
向量化不只是代码短,是真的快。把循环交给底层,绕开 Python 一层层的解释开销。
五、广播:形状不一样也能算
2 行 3 列的矩阵加一个数 100,每行都变成 [101, 102, 103] 和 [104, 105, 106]。 加一个长度 3 的数组 [10, 20, 30],第一行变 [11, 22, 33],第二行变 [14, 25, 36]。 广播的规则:从最后一维往前对,长度相等,或者其中一个是 1。
对不上就报错,不会凑合算。原文是 operands could not be broadcast together。 2 行 3 列加长度 2 的数组就报这个错,因为最后一维 3 和 2 对不上。 报错时先看两边形状,从最后一维往前对一遍,问题立刻现形。
六、动手 Demo:数据类型与数组运算
先看 dtype、itemsize、nbytes 三个属性怎么互相对上,再试塞爆 int8。 然后跑逐元素运算、100 万个数乘 2 的对比,最后看广播和对不上的报错。
import time
import numpy as np
A = np.array([1, 2, 3])
print("dtype %s,每元素 %d 字节,整块 %d 字节" % (A.dtype, A.itemsize, A.nbytes))
print("转成 float:%s" % A.astype(float))
print("原数组还是 %s" % A)
try:
np.array([200], dtype=np.int8)
except OverflowError:
print("塞 200 进 int8:OverflowError,上限 127")
A = np.array([1, 2, 3])
B = np.array([10, 20, 30])
print("A + B = %s" % (A + B))
print("A * B = %s(逐元素)" % (A * B))
print("A 的平方 = %s" % np.square(A))
print("B / A = %s" % (B / A))
N = 1000000
L = list(range(N))
BIG = np.arange(N)
def t(fn):
for _ in range(3):
fn()
best = 1e9
for _ in range(5):
s = time.perf_counter()
fn()
best = min(best, time.perf_counter() - s)
return best
t1 = t(lambda: [x * 2 for x in L])
t2 = t(lambda: BIG * 2)
print("100 万数乘 2:列表 %.1f ms,数组 %.1f ms,快 %.0f 倍"
% (t1 * 1000, t2 * 1000, t1 / t2))
M = np.array([[1, 2, 3], [4, 5, 6]])
print("加一个数:")
print(M + 100)
print("加长度 3 的数组:")
print(M + np.array([10, 20, 30]))
try:
M + np.array([1, 2])
except ValueError as e:
print("加长度 2 的数组:%s" % str(e).split(chr(10))[0][:70])
实跑输出:
--------------------------------------------------------------------
数据类型与数组运算
--------------------------------------------------------------------
① 类型与查看
--------------------------------------------------------------------
[1 2 3] 的 dtype 是 int64,itemsize 8 字节,nbytes 24 字节。
转成 float:[1. 2. 3.]
想塞 200 进 int8:直接 OverflowError。int8 上限 127。
新版 numpy 直接报错;老版本会静默溢出成 -56(200 减 256)。
静默溢出比报错危险得多——错的那一位就这样混进后面的计算里。
! astype 是复制,不是原地改。要生效得接住返回值。
② 逐元素运算
--------------------------------------------------------------------
A + B = [11 22 33]
A * B = [10 40 90](逐元素相乘,不是点积)
A 的平方 = [1 4 9]
B / A = [10. 10. 10.]
! 星号是逐元素乘,矩阵乘法要写 @ 或 dot。这是 NumPy 最容易错的一处。
③ 和列表比:省掉循环
--------------------------------------------------------------------
100 万个数乘 2:列表推导 118.8 毫秒,数组 2.1 毫秒,快 55 倍。
数组不用写循环,一行 A * 2 就整块算完。这叫「向量化」。
! 向量化不只是短,是真的快。同样的算法,绕开 Python 的循环就赢一大截。
④ 广播:形状不一样也能算
--------------------------------------------------------------------
2 行 3 列的矩阵,加一个数:
[101, 102, 103]
[104, 105, 106]
加一个长度 3 的数组:
[11, 22, 33]
[14, 25, 36]
广播的规则:从最后一维往前对,能对上就铺开。对不上报错。
! 广播让「每行减自己的均值」这类操作不用写循环:A 减 A.mean(axis=1, keepdims=True)。
⑤ 广播对不上会怎样
--------------------------------------------------------------------
2 行 3 列加长度 2 的数组:报 ValueError。
原文:operands could not be broadcast together with shapes (2,3) (2,)
规则对不上就报错,不会「凑合算」。这是好事——错了马上知道。
! 报错时先看两边形状:从最后一维往前对,长度要么相等,要么其中一个是 1。
np.square(A) 就是 A 的平方,写法比双星号运算符更安全,不会被当成加粗标记。
int8 的溢出这一条要记牢:新版本报错,老版本静默给出 -56。报错比给出错数好。
55 倍这个数只对「100 万个整数乘 2」这一件事成立,换运算换规模都会变。
30 秒实验
- 把 numpy 版本、机器都没变的情况下,把 N 改成 100 万再改成 10 万,看倍数怎么变。
- 把 A 改成 int8 数组,再往里面加 100,看会不会溢出。
- 想一想:广播为什么允许「其中一个是 1」?它把那一维当成重复了几遍?
常见误区
- 以为
A * B是矩阵乘:它只是逐元素乘,矩阵乘要写 @。 - 不接 astype 的返回值:转完类型就丢了,原数组根本没变。
- 看到 dtype 是 object 不当回事:多半是混进了字符串或空值,后面算不动。
- 以为溢出会报错:新版本会,老版本不会,会把错数悄悄带下去。
自测三问(点开对答案)
1. nbytes、itemsize、元素个数三者是什么关系? —— nbytes 等于 itemsize 乘元素个数。8 乘 3 得 24。 2. 往 int8 里塞 200,新版和老版分别怎样? —— 新版抛 OverflowError;老版静默溢出成 -56,也就是 200 减 256。 3. 广播对不上时,第一步该看什么? —— 看两边的形状,从最后一维往前对,长度要么相等,要么其中一个是 1。运算会了,接下来是怎么把要的那部分取出来。下一篇:索引与切片,有一个视图的坑。
- ☐ (本篇完,下一篇 6.7 索引与切片)