import numpy as np
x = np.linspace(0, 2*np.pi, 10)
对数组x中的每个元素进行正弦计算,返回一个同样大小的新数组:
y = np.sin(x)
y
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01, 8.66025404e-01,
3.42020143e-01, -3.42020143e-01, -8.66025404e-01, -9.84807753e-01,
-6.42787610e-01, -2.44929360e-16])先用 linspace 产生一个从 0 到 2*PI 的等距离的10个数,然后将其传递给 sin 函数。
由于 np.sin 是一个 ufunc 函数,因此它对 x 中的每个元素求正弦值,然后将结果返回,并且赋值给 y 。 计算之后 x 中的值并没有改变,而是新创建了一个数组保存结果。
NumPy 提供了一种节省内存的方式。 如果我们希望将 sin 函数所计算的结果直接覆盖到数组 x 上去的话,可以将要被覆盖的数组作为第二个参数传递给 ufunc函数。
例如:
t = np.sin(x,x)
x
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01, 8.66025404e-01,
3.42020143e-01, -3.42020143e-01, -8.66025404e-01, -9.84807753e-01,
-6.42787610e-01, -2.44929360e-16])t
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01, 8.66025404e-01,
3.42020143e-01, -3.42020143e-01, -8.66025404e-01, -9.84807753e-01,
-6.42787610e-01, -2.44929360e-16])查看一下内存中 t 与 x 的地址ID:
id(t) == id(x)
True
sin 函数的第二个参数也是x,那么它所做的事情就是对x中的每给值求正弦值,并且把结果保存到x中的对应的位置中。 此时函数的返回值仍然是整个计算的结果,只不过它就是x,因此两个变量的id是相同的(变量t和变量x指向同一块内存区域)。
为了对运算速度进行说明,使用下面的程序比较了 numpy.math 和 Python 标准库中 math.sin 的计算速度:
import time
import math
x = [i * 0.001 for i in range(1000000)]
start = time.time()
for i, t in enumerate(x):
x[i] = math.sin(t)
t1 = time.time() - start
print("math.sin:", t1)
math.sin: 0.6212267875671387
x = np.array(x)
start = time.time()
np.sin(x,x)
t2 = time.time() - start
print("numpy.sin:", t2)
numpy.sin: 0.06662464141845703
在我的电脑上计算100万次正弦值,numpy.sin 比 math.sin 快10倍多:
t1 / t2
9.324279641001418
这得利于 numpy.sin 在C语言级别的循环计算。 numpy.sin 同样也支持对单个数值求正弦,例如: numpy.sin(0.5) 。 不过值得注意的是,对单个数的计算 math.sin 则比 numpy.sin 快得多了,让我们看下面这个测试程序:
x = [i * 0.001 for i in range(1000000)]
start = time.time()
for i, t in enumerate(x):
x[i] = np.sin(t)
print("numpy.sin loop:", time.time() - start)
numpy.sin loop: 1.7610507011413574
请注意 numpy.sin 的计算速度只有 math.sin 的1/5。
这是因为 numpy.sin 为了同时支持数组和单个值的计算,其C语言的内部实现要比 math.sin 复杂很多, 如果我们同样在Python级别进行循环的话,就会看出其中的差别了。
此外,numpy.sin 返回的数的类型和 math.sin 返回的类型有所不同, math.sin 返回的是Python的标准 float 类型,而 numpy.sin 则返回一个 numpy.float64 类型:
type(math.sin(0.5))
float
type(np.sin(0.5))
numpy.float64
a = np.arange(0,4)
a
array([0, 1, 2, 3])
b = np.arange(1,5)
b
array([1, 2, 3, 4])
np.add(a,b)
array([1, 3, 5, 7])
np.add(a,b,a)
array([1, 3, 5, 7])
a
array([1, 3, 5, 7])
add 函数返回一个新的数组,此数组的每个元素都为两个参数数组的对应元素之和。 它接受第3个参数指定计算结果所要写入的数组,如果指定的话, add 函数就不再产生新的数组。 由于Python的操作符重载功能,计算两个数组相加可以简单地写为 a+b ,而 np.add(a,b,a) 则可以用 a+=b 来表示。 下面是数组的运算符和其对应的 ufunc 函数的一个列表, 注意除号 ''/'' 的意义根据是否激活 __future__.division 有所不同。
| y = x1 + x2: | add(x1, x2 [, y]) |
| y = x1 - x2: | subtract(x1, x2 [, y]) |
| y = x1 * x2: | multiply (x1, x2 [, y]) |
| y = x1 / x2: | divide (x1, x2 [, y]), 如果两个数组的元素为整数,那么用整数除法 |
| y = x1 / x2: | true divide (x1, x2 [, y]), 总是返回精确的商 |
| y = x1 // x2: | floor divide (x1, x2 [, y]), 总是对返回值取整 |
| y = -x: | negative(x [,y]) |
| y = x1**x2: | power(x1, x2 [, y]) |
| y = x1 % x2: | remainder(x1, x2 [, y]), mod(x1, x2, [, y]) |
数组对象支持这些操作符,极大地简化了算式的编写,不过要注意如果算式很复杂, 并且要运算的数组很大,会因为产生大量的中间结果而降低程序的运算效率。 例如:假设a b c三个数组采用算式 x=a*b+c 计算,那么它相当于:
t = a * b
x = t + c
del t也就是说需要产生一个数组 t 保存乘法的计算结果,然后再产生最后的结果数组 x 。 我们可以通过手工将一个算式分解为 x = a*b ; x += c ,以减少一次内存分配。 通过组合标准的 ufunc 函数的调用,可以实现各种算式的数组计算。不过有些时候这种算式不易编写, 而针对每个元素的计算函数却很容易用Python实现, 这时可以用 frompyfunc 函数将一个计算单个元素的函数转换成 ufunc 函数。 这样就可以方便地用所产生的 ufunc 函数对数组进行计算了。让我们来看一个例子。 我们想用一个分段函数描述三角波,三角波的样子如图2.4所示:

图 2.4 - 三角波可以用分段函数进行计算
我们很容易根据上图所示写出如下的计算三角波某点y坐标的函数:
三角波的周期为1,因此只取x坐标的小数部分进行计算
def triangle_wave(x, c, c0, hc):
x = x - int(x)
if x >= c:
r = 0.0
elif x < c0:
r = x / c0 * hc
else:
r = (c-x) / (c-c0) * hc
return r
显然 triangle_wave 函数只能计算单个数值,不能对数组直接进行处理。 我们可以用下面的方法先使用列表包容(List comprehension),计算出一个 list , 然后用 array 函数将列表转换为数组:
x = np.linspace(0, 2, 1000)
y = np.array([triangle_wave(t, 0.6, 0.4, 1.0) for t in x])
这种做法每次都需要使用列表包容语法调用函数,对于多维数组是很麻烦的。 让我们来看看如何用 frompyfunc 函数来解决这个问题:
triangle_ufunc = np.frompyfunc( lambda x: triangle_wave(x, 0.6, 0.4, 1.0), 1, 1)
y2 = triangle_ufunc(x)
frompyfunc 的调用格式为 frompyfunc(func, nin, nout) ,其中 func 是计算单个元素的函数, nin 是此函数的输入参数的个数, nout 是此函数的返回值的个数。虽然 triangle_wave 函数有4个参数, 但是由于后三个c, c0, hc在整个计算中值都是固定的,因此所产生的 ufunc 函数其实只有一个参数。 为了满足这个条件,我们用一个 lambda 函数对 triangle_wave 的参数进行一次包装。 这样传入 frompyfunc 的函数就只有一个参数了。这样做,效率并不是太高,另外还有一种方法:
def triangle_func(c, c0, hc):
def trifunc(x):
x = x - int(x) # 三角波的周期为1,因此只取x坐标的小数部分进行计算
if x >= c: r = 0.0
elif x < c0: r = x / c0 * hc
else: r = (c-x) / (c-c0) * hc
return r
# 用trifunc函数创建一个ufunc函数,可以直接对数组进行计算, 不过通过此函数
# 计算得到的是一个Object数组,需要进行类型转换
return np.frompyfunc(trifunc, 1, 1)
y2 = triangle_func(0.6, 0.4, 1.0)(x)
我们通过函数 triangle_func 包装三角波的三个参数,在其内部定义一个计算三角波的函数 trifunc , trifunc 函数在调用时会采用 triangle_func 的参数进行计算。 最后 triangle_func 返回用 frompyfunc 转换结果。
值得注意的是用 frompyfunc 得到的函数计算出的数组元素的类型为 object , 因为 frompyfunc 函数无法保证Python函数返回的数据类型都完全一致。 因此还需要再次 y2.astype(np.float64) 将其转换为双精度浮点数组。
广播 (broadcasting)
broadcasting 的翻译,很难理解其意义。
对于英文来讲,其实是 broad + cast 两个单词的拼写。 这两个单词放在一起恰巧是英文单词 broadcast ,但据此将其翻译为“广播”,也应理解其意义并非“广泛传播”。
cast 在编程中非常重要,中文一般称为“造型”,一般是指程序中数据类型的转换。 而在 NumPy 中, 此处的操作并非是数据类型的转换,而是数组长度的对齐,以满足数组计算。 而这个对齐都是由短往长变的,即为 broad 的意义。
当我们使用 ufunc 函数对两个数组进行计算时, ufunc 函数会对这两个数组的对应元素进行计算, 因此它要求这两个数组有相同的大小(shape相同)。如果两个数组的 shape 不同的话, 会进行如下的广播(broadcasting)处理:
- 让所有输入数组都向其中
shape最长的数组看齐,shape中不足的部分都通过在前面加1补齐 - 输出数组的
shape是输入数组shape的各个轴上的最大值 - 如果输入数组的某个轴和输出数组的对应轴的长度相同或者其长度为1时,这个数组能够用来计算,否则出错
- 当输入数组的某个轴的长度为1时,沿着此轴运算时都用此轴上的第一组值
上述4条规则理解起来可能比较费劲,让我们来看一个实际的例子。 先创建一个二维数组 a ,其shape为 (6,1) :
a = np.arange(0, 60, 10).reshape(-1, 1)
a
array([[ 0],
[10],
[20],
[30],
[40],
[50]])a.shape
(6, 1)
再创建一维数组b,其 shape 为 (5,) :
b = np.arange(0, 5)
b
array([0, 1, 2, 3, 4])
b.shape
(5,)
计算a和b的和,得到一个加法表,它相当于计算a,b中所有元素组的和, 得到一个 shape 为 (6,5) 的数组:
c = a + b
c
array([[ 0, 1, 2, 3, 4],
[10, 11, 12, 13, 14],
[20, 21, 22, 23, 24],
[30, 31, 32, 33, 34],
[40, 41, 42, 43, 44],
[50, 51, 52, 53, 54]])c.shape
(6, 5)
由于a和b的 shape 长度(也就是 ndim 属性)不同,根据规则1,需要让b的 shape 向a对齐, 于是将b的 shape 前面加1,补齐为(1,5)。相当于做了如下计算:
b.shape=1,5
b
array([[0, 1, 2, 3, 4]])
这样加法运算的两个输入数组的 shape 分别为 (6,1) 和 (1,5) ,根据规则2, 输出数组的各个轴的长度为输入数组各个轴上的长度的最大值,可知输出数组的 shape 为 (6,5) 。 由于b的第0轴上的长度为1,而a的第0轴上的长度为6,因此为了让它们在第0轴上能够相加, 需要将b在第0轴上的长度扩展为6,这相当于:
b = b.repeat(6,axis=0)
b
array([[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4]])由于 a 的第1轴的长度为 1 ,而 b 的第一轴长度为 5 ,因此为了让它们在第1轴上能够相加, 需要将 a 在第1轴上的长度扩展为 5 ,这相当于:
a = a.repeat(5, axis=1)
a
array([[ 0, 0, 0, 0, 0],
[10, 10, 10, 10, 10],
[20, 20, 20, 20, 20],
[30, 30, 30, 30, 30],
[40, 40, 40, 40, 40],
[50, 50, 50, 50, 50]])经过上述处理之后,a和b就可以按对应元素进行相加运算了。当然,numpy 在执行 a+b 运算时, 其内部并不会真正将长度为1的轴用 repeat 函数进行扩展,如果这样做的话就太浪费空间了。
由于这种广播计算很常用,因此 numpy 提供了一个快速产生如上面a,b数组的方法: ogrid 对象:
x,y = np.ogrid[0:5,0:5]
x
array([[0],
[1],
[2],
[3],
[4]])y
array([[0, 1, 2, 3, 4]])
ogrid 是一个很有趣的对象,它像一个多维数组一样,用切片组元作为下标进行存取,返回的是一组可以用来广播计算的数组。 其切片下标有两种形式:
- 开始值:结束值:步长,和
np.arange(开始值, 结束值, 步长)类似 - 开始值:结束值:长度j,当第三个参数为虚数时,它表示返回的数组的长度,和
np.linspace(开始值, 结束值, 长度)类似:
x, y = np.ogrid[0:1:4j, 0:1:3j]
x
array([[0. ],
[0.33333333],
[0.66666667],
[1. ]])y
array([[0. , 0.5, 1. ]])
根据Python的语法,只有在中括号中才能使用用冒号隔开的切片语法,如果 ogrid 是函数的话, 那么这些切片必须使用 slice 函数创建,这显然会增加代码的长度。
利用 ogrid 的返回值,我能很容易计算x, y网格面上各点的值,或者x, y, z网格体上各点的值。 下面是绘制三维曲面 x * exp(x**2 - y**2) 的程序:
import numpy as np
from enthought.mayavi import mlab
x, y = np.ogrid[-2:2:20j, -2:2:20j]
z = x * np.exp( - x**2 - y**2)
pl = mlab.surf(x, y, z, warp_scale="auto")
mlab.axes(xlabel='x', ylabel='y', zlabel='z')
mlab.outline(pl)<op>.reduce (array=, axis=0, dtype=None)例如:
np.add.reduce([1,2,3]) # 1 + 2 + 3
np.int64(6)
np.add.reduce([[1,2,3],[4,5,6]], axis=1) # 1,4 + 2,5 + 3,6
array([ 6, 15])
accumulate 方法和 reduce 方法类似,只是它返回的数组和输入的数组的 shape 相同, 保存所有的中间计算结果:
np.add.accumulate([1,2,3])
array([1, 3, 6])
np.add.accumulate([[1,2,3],[4,5,6]], axis=1)
array([[ 1, 3, 6],
[ 4, 9, 15]])reduceat 方法计算多组 reduce 的结果,通过 indices 参数指定一系列 reduce 的起始和终了位置。 reduceat 的计算有些特别,让我们通过一个例子来解释一下:
a = np.array([1,2,3,4])
result = np.add.reduceat(a,indices=[0,1,0,2,0,3,0])
result
array([ 1, 2, 3, 3, 6, 4, 10])
对于 indices 中的每个元素都会调用 reduce 函数计算出一个值来, 因此最终计算结果的长度和 indices 的长度相同。 结果 result 数组中除最后一个元素之外,都按照如下计算得出:
if indices[i] < indices[i+1]:
result[i] = np.reduce(a[indices[i]:indices[i+1]])
else:
result[i] = a[indices[i]而最后一个元素如下计算:
np.reduce(a[indices[-1]:])因此上面例子中,结果的每个元素如下计算而得:
1 : a[0] = 1
2 : a[1] = 2
3 : a[0] + a[1] = 1 + 2
3 : a[2] = 3
6 : a[0] + a[1] + a[2] =
1 + 2 + 3 = 6
4 : a[3] = 4
10: a[0] + a[1] + a[2] + a[4] = 1+2+3+4 = 10可以看出 result[::2] 和 a 相等,而 result[1::2] 和 np.add.accumulate(a) 相等。 outer 方法, <op>.outer(a,b) 方法的计算等同于如下程序:
a.shape += (1,)*b.ndim
<op>(a,b)
a = a.squeeze()其中 squeeze 的功能是剔除数组a中长度为1的轴。如果你看不太明白这个等同程序的话, 让我们来看一个例子:
np.multiply.outer([1,2,3,4,5],[2,3,4])
array([[ 2, 3, 4],
[ 4, 6, 8],
[ 6, 9, 12],
[ 8, 12, 16],
[10, 15, 20]])可以看出通过 outer 方法计算的结果是如下的乘法表:
#
2, 3, 4
# 1
# 2
# 3
# 4
# 5如果将这两个数组按照等同程序一步一步的计算的话,就会发现乘法表最终是通过广播的方式计算出来的。
