遍历数组#
迭代器对象 nditer
(在 NumPy 1.6 中引入)提供了许多灵活的方式,可以系统地访问一个或多个数组的所有元素。本页将介绍在 Python 中使用该对象进行数组计算的一些基本方法,然后讨论如何在 Cython 中加速内部循环。由于 nditer
的 Python 接口是 C 数组迭代器 API 的一个相对直接的映射,因此这些概念也将有助于从 C 或 C++ 进行数组迭代操作。
单数组迭代#
使用 nditer
可以完成的最基本任务是访问数组的每个元素。每个元素都通过标准的 Python 迭代器接口逐一提供。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> for x in np.nditer(a):
... print(x, end=' ')
...
0 1 2 3 4 5
这种迭代需要注意的一点是,其顺序是根据数组的内存布局选择的,而不是使用标准的 C 顺序或 Fortran 顺序。这样做是为了提高访问效率,反映了默认情况下用户只是希望访问每个元素而不关心特定顺序的想法。我们可以通过迭代我们之前数组的转置来看到这一点,并将其与以 C 顺序复制该转置进行比较。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> for x in np.nditer(a.T):
... print(x, end=' ')
...
0 1 2 3 4 5
>>> for x in np.nditer(a.T.copy(order='C')):
... print(x, end=' ')
...
0 3 1 4 2 5
a 和 a.T 的元素都以相同的顺序遍历,即它们在内存中存储的顺序,而 a.T.copy(order=’C’) 的元素则以不同的顺序访问,因为它们已被放置在不同的内存布局中。
控制迭代顺序#
有时,以特定顺序访问数组元素很重要,无论元素在内存中的布局如何。nditer
对象提供了一个 order 参数来控制迭代的这一方面。默认行为是保持现有顺序,即 order='K'。这可以通过 order='C'(C 顺序)和 order='F'(Fortran 顺序)进行覆盖。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> for x in np.nditer(a, order='F'):
... print(x, end=' ')
...
0 3 1 4 2 5
>>> for x in np.nditer(a.T, order='C'):
... print(x, end=' ')
...
0 3 1 4 2 5
修改数组值#
默认情况下,nditer
将输入操作数视为只读对象。要修改数组元素,您必须使用 ‘readwrite’ 或 ‘writeonly’ 的每个操作数标志来指定读写或只写模式。
然后 nditer 将生成可写的缓冲区数组,您可以修改它们。然而,由于 nditer 在迭代完成后必须将此缓冲区数据复制回原始数组,因此您必须通过以下两种方法之一来指示迭代何时结束。您可以选择:
一旦调用了 close
或其上下文退出,nditer 将无法再进行迭代。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
>>> with np.nditer(a, op_flags=['readwrite']) as it:
... for x in it:
... x[...] = 2 * x
...
>>> a
array([[ 0, 2, 4],
[ 6, 8, 10]])
如果您正在编写需要支持旧版本 NumPy 的代码,请注意,在 1.15 版本之前,nditer
不是上下文管理器,也没有 close
方法。它依赖于析构函数来启动缓冲区的写回。
使用外部循环#
到目前为止的所有示例中,a 的元素都是由迭代器一次提供一个的,因为所有的循环逻辑都在迭代器内部。虽然这简单方便,但效率不高。更好的方法是将一维的最内层循环移到您的代码中,放在迭代器外部。这样,NumPy 的向量化操作就可以用于访问更大块的元素。
nditer
将尝试向内部循环提供尽可能大的数据块。通过强制使用 'C' 和 'F' 顺序,我们获得了不同的外部循环大小。通过指定迭代器标志可以启用此模式。
请注意,在默认保持原生内存顺序的情况下,迭代器能够提供一个单一的一维数据块,而当强制使用 Fortran 顺序时,它必须提供三个包含两个元素的块。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> for x in np.nditer(a, flags=['external_loop']):
... print(x, end=' ')
...
[0 1 2 3 4 5]
>>> for x in np.nditer(a, flags=['external_loop'], order='F'):
... print(x, end=' ')
...
[0 3] [1 4] [2 5]
跟踪索引或多维索引#
在迭代过程中,您可能希望在计算中使用当前元素的索引。例如,您可能希望按内存顺序访问数组的元素,但使用 C 顺序、Fortran 顺序或多维索引来查找不同数组中的值。
索引由迭代器对象本身跟踪,并可通过 index 或 multi_index 属性访问,具体取决于请求的内容。下面的示例显示了演示索引进展的打印输出。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> it = np.nditer(a, flags=['f_index'])
>>> for x in it:
... print("%d <%d>" % (x, it.index), end=' ')
...
0 <0> 1 <2> 2 <4> 3 <1> 4 <3> 5 <5>
>>> it = np.nditer(a, flags=['multi_index'])
>>> for x in it:
... print("%d <%s>" % (x, it.multi_index), end=' ')
...
0 <(0, 0)> 1 <(0, 1)> 2 <(0, 2)> 3 <(1, 0)> 4 <(1, 1)> 5 <(1, 2)>
>>> with np.nditer(a, flags=['multi_index'], op_flags=['writeonly']) as it:
... for x in it:
... x[...] = it.multi_index[1] - it.multi_index[0]
...
>>> a
array([[ 0, 1, 2],
[-1, 0, 1]])
跟踪索引或多维索引与使用外部循环不兼容,因为它要求每个元素有一个不同的索引值。如果您尝试组合这些标志,nditer
对象将引发异常。
示例
>>> import numpy as np
>>> a = np.zeros((2,3))
>>> it = np.nditer(a, flags=['c_index', 'external_loop'])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Iterator flag EXTERNAL_LOOP cannot be used if an index or multi-index is being tracked
替代的循环和元素访问#
为了在迭代期间更方便地访问其属性,nditer
提供了一种替代的迭代语法,它直接与迭代器对象本身配合使用。通过这种循环构造,可以通过索引迭代器来访问当前值。其他属性,例如跟踪的索引,保持不变。下面的示例将产生与前一节中相同的结果。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> it = np.nditer(a, flags=['f_index'])
>>> while not it.finished:
... print("%d <%d>" % (it[0], it.index), end=' ')
... is_not_finished = it.iternext()
...
0 <0> 1 <2> 2 <4> 3 <1> 4 <3> 5 <5>
>>> it = np.nditer(a, flags=['multi_index'])
>>> while not it.finished:
... print("%d <%s>" % (it[0], it.multi_index), end=' ')
... is_not_finished = it.iternext()
...
0 <(0, 0)> 1 <(0, 1)> 2 <(0, 2)> 3 <(1, 0)> 4 <(1, 1)> 5 <(1, 2)>
>>> with np.nditer(a, flags=['multi_index'], op_flags=['writeonly']) as it:
... while not it.finished:
... it[0] = it.multi_index[1] - it.multi_index[0]
... is_not_finished = it.iternext()
...
>>> a
array([[ 0, 1, 2],
[-1, 0, 1]])
缓冲数组元素#
当强制迭代顺序时,我们观察到外部循环选项可能会以更小的块提供元素,因为元素无法以恒定步长按适当顺序访问。在编写 C 代码时,这通常没有问题,但在纯 Python 代码中,这可能会导致性能显著下降。
通过启用缓冲模式,迭代器提供给内部循环的数据块可以更大,显著减少 Python 解释器的开销。在强制 Fortran 迭代顺序的示例中,当启用缓冲时,内部循环可以一次性看到所有元素。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3)
>>> for x in np.nditer(a, flags=['external_loop'], order='F'):
... print(x, end=' ')
...
[0 3] [1 4] [2 5]
>>> for x in np.nditer(a, flags=['external_loop','buffered'], order='F'):
... print(x, end=' ')
...
[0 3 1 4 2 5]
以特定数据类型迭代#
有时需要将数组视为与其存储类型不同的数据类型。例如,即使操作的数组是 32 位浮点数,也可能希望在 64 位浮点数上进行所有计算。除非编写底层 C 代码,否则通常最好让迭代器处理复制或缓冲,而不是在内部循环中自行转换数据类型。
有两种机制可以实现这一点:临时复制和缓冲模式。使用临时复制时,会创建整个数组的新数据类型副本,然后在副本中进行迭代。通过一种在所有迭代完成后更新原始数组的模式,允许写入访问。临时复制的主要缺点是临时副本可能会消耗大量内存,特别是如果迭代数据类型比原始数据类型具有更大的 itemsize。
缓冲模式减轻了内存使用问题,并且比临时复制更具缓存友好性。除了在迭代器外部一次性需要整个数组的特殊情况外,建议使用缓冲而不是临时复制。在 NumPy 内部,ufuncs 和其他函数使用缓冲来支持灵活输入,同时将内存开销降至最低。
在我们的示例中,我们将使用复杂数据类型处理输入数组,以便我们可以计算负数的平方根。如果不启用复制或缓冲模式,如果数据类型不精确匹配,迭代器将引发异常。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3) - 3
>>> for x in np.nditer(a, op_dtypes=['complex128']):
... print(np.sqrt(x), end=' ')
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Iterator operand required copying or buffering, but neither copying nor buffering was enabled
在复制模式下,'copy' 被指定为每个操作数的标志。这样做是为了以每个操作数的方式提供控制。缓冲模式被指定为迭代器标志。
示例
>>> import numpy as np
>>> a = np.arange(6).reshape(2,3) - 3
>>> for x in np.nditer(a, op_flags=['readonly','copy'],
... op_dtypes=['complex128']):
... print(np.sqrt(x), end=' ')
...
1.7320508075688772j 1.4142135623730951j 1j 0j (1+0j) (1.4142135623730951+0j)
>>> for x in np.nditer(a, flags=['buffered'], op_dtypes=['complex128']):
... print(np.sqrt(x), end=' ')
...
1.7320508075688772j 1.4142135623730951j 1j 0j (1+0j) (1.4142135623730951+0j)
迭代器使用 NumPy 的类型转换规则来确定是否允许特定的转换。默认情况下,它强制执行 'safe'(安全)类型转换。这意味着,例如,如果您尝试将 64 位浮点数组视为 32 位浮点数组,它将引发异常。在许多情况下,'same_kind'(同类)规则是最合理的规则,因为它允许从 64 位浮点数转换为 32 位浮点数,但不能从浮点数转换为整数或从复数转换为浮点数。
示例
>>> import numpy as np
>>> a = np.arange(6.)
>>> for x in np.nditer(a, flags=['buffered'], op_dtypes=['float32']):
... print(x, end=' ')
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Iterator operand 0 dtype could not be cast from dtype('float64') to dtype('float32') according to the rule 'safe'
>>> for x in np.nditer(a, flags=['buffered'], op_dtypes=['float32'],
... casting='same_kind'):
... print(x, end=' ')
...
0.0 1.0 2.0 3.0 4.0 5.0
>>> for x in np.nditer(a, flags=['buffered'], op_dtypes=['int32'], casting='same_kind'):
... print(x, end=' ')
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Iterator operand 0 dtype could not be cast from dtype('float64') to dtype('int32') according to the rule 'same_kind'
需要注意的一点是,在使用读写或只写操作数时,转换回原始数据类型。一个常见的案例是使用 64 位浮点数实现内部循环,并使用 'same_kind' 类型转换来允许处理其他浮点类型。虽然在只读模式下可以提供整数数组,但读写模式将引发异常,因为转换回数组会违反类型转换规则。
示例
>>> import numpy as np
>>> a = np.arange(6)
>>> for x in np.nditer(a, flags=['buffered'], op_flags=['readwrite'],
... op_dtypes=['float64'], casting='same_kind'):
... x[...] = x / 2.0
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: Iterator requested dtype could not be cast from dtype('float64') to dtype('int64'), the operand 0 dtype, according to the rule 'same_kind'
广播数组迭代#
NumPy 有一套处理形状不同数组的规则,只要函数接受多个按元素组合的操作数,这些规则就会被应用。这称为广播。nditer
对象可以在您需要编写此类函数时为您应用这些规则。
例如,我们打印出将一维数组和二维数组一起广播的结果。
示例
>>> import numpy as np
>>> a = np.arange(3)
>>> b = np.arange(6).reshape(2,3)
>>> for x, y in np.nditer([a,b]):
... print("%d:%d" % (x,y), end=' ')
...
0:0 1:1 2:2 0:3 1:4 2:5
当发生广播错误时,迭代器会引发一个异常,其中包含输入形状以帮助诊断问题。
示例
>>> import numpy as np
>>> a = np.arange(2)
>>> b = np.arange(6).reshape(2,3)
>>> for x, y in np.nditer([a,b]):
... print("%d:%d" % (x,y), end=' ')
...
Traceback (most recent call last):
...
ValueError: operands could not be broadcast together with shapes (2,) (2,3)
迭代器分配的输出数组#
NumPy 函数中一个常见情况是根据输入广播分配输出,并且额外有一个可选参数 'out',如果提供该参数,结果将放置在该位置。nditer
对象提供了一种方便的用法,使得支持这种机制变得非常容易。
我们将通过创建一个函数 square
来展示其工作原理,该函数计算其输入的平方。让我们从一个不包含 'out' 参数支持的最小函数定义开始。
示例
>>> import numpy as np
>>> def square(a):
... with np.nditer([a, None]) as it:
... for x, y in it:
... y[...] = x*x
... return it.operands[1]
...
>>> square([1,2,3])
array([1, 4, 9])
默认情况下,nditer
对作为 None 传入的操作数使用 'allocate' 和 'writeonly' 标志。这意味着我们只需向迭代器提供两个操作数,其余的由它处理。
添加 'out' 参数时,我们必须显式提供这些标志,因为如果有人将数组作为 'out' 传入,迭代器将默认为 'readonly',我们的内部循环将失败。输入数组默认为 'readonly' 的原因是为了防止意外触发归约操作造成的混淆。如果默认为 'readwrite',任何广播操作也会触发归约,这是一个将在本文档后面讨论的话题。
顺便一提,我们还引入了 'no_broadcast' 标志,它将阻止输出被广播。这很重要,因为我们每个输出只希望有一个输入值。聚合多个输入值是一种需要特殊处理的归约操作。它本来就会引发错误,因为归约必须在迭代器标志中显式启用,但禁用广播所产生的错误消息对于最终用户来说更容易理解。要了解如何将平方函数推广到归约操作,请参阅关于 Cython 的部分中的平方和函数。
为了完整性,我们还将添加 'external_loop' 和 'buffered' 标志,因为这些通常是出于性能考虑您会需要的。
示例
>>> import numpy as np
>>> def square(a, out=None):
... it = np.nditer([a, out],
... flags = ['external_loop', 'buffered'],
... op_flags = [['readonly'],
... ['writeonly', 'allocate', 'no_broadcast']])
... with it:
... for x, y in it:
... y[...] = x*x
... return it.operands[1]
...
>>> square([1,2,3])
array([1, 4, 9])
>>> b = np.zeros((3,))
>>> square([1,2,3], out=b)
array([1., 4., 9.])
>>> b
array([1., 4., 9.])
>>> square(np.arange(6).reshape(2,3), out=b)
Traceback (most recent call last):
...
ValueError: non-broadcastable output operand with shape (3,) doesn't
match the broadcast shape (2,3)
外积迭代#
任何二元操作都可以像 outer
一样以外积方式扩展到数组操作,nditer
对象提供了一种通过显式映射操作数轴来完成此操作的方法。虽然也可以使用 newaxis
索引来完成此操作,但我们将向您展示如何直接使用 nditer 的 op_axes 参数来实现,而无需中间视图。
我们将进行一个简单的外积,将第一个操作数的维度放在第二个操作数的维度之前。op_axes 参数需要为每个操作数提供一个轴列表,并提供从迭代器轴到操作数轴的映射。
假设第一个操作数是一维的,第二个操作数是二维的。迭代器将有三个维度,因此 op_axes 将有两个 3 元素列表。第一个列表选出第一个操作数的一个轴,其余迭代器轴为 -1,最终结果为 [0, -1, -1]。第二个列表选出第二个操作数的两个轴,但不得与第一个操作数中选出的轴重叠。它的列表是 [-1, 0, 1]。输出操作数以标准方式映射到迭代器轴,因此我们可以提供 None 而不是构造另一个列表。
内部循环中的操作是直接的乘法。所有与外积相关的事情都由迭代器设置处理。
示例
>>> import numpy as np
>>> a = np.arange(3)
>>> b = np.arange(8).reshape(2,4)
>>> it = np.nditer([a, b, None], flags=['external_loop'],
... op_axes=[[0, -1, -1], [-1, 0, 1], None])
>>> with it:
... for x, y, z in it:
... z[...] = x*y
... result = it.operands[2] # same as z
...
>>> result
array([[[ 0, 0, 0, 0],
[ 0, 0, 0, 0]],
[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 0, 2, 4, 6],
[ 8, 10, 12, 14]]])
请注意,一旦迭代器关闭,我们就无法访问 operands
,必须使用在上下文管理器中创建的引用。
归约迭代#
每当一个可写操作数的元素少于完整迭代空间时,该操作数就正在进行归约。nditer
对象要求任何归约操作数都必须标记为读写,并且只有当提供 'reduce_ok' 作为迭代器标志时才允许进行归约。
举一个简单的例子,考虑计算数组中所有元素的和。
示例
>>> import numpy as np
>>> a = np.arange(24).reshape(2,3,4)
>>> b = np.array(0)
>>> with np.nditer([a, b], flags=['reduce_ok'],
... op_flags=[['readonly'], ['readwrite']]) as it:
... for x,y in it:
... y[...] += x
...
>>> b
array(276)
>>> np.sum(a)
276
当组合归约和已分配的操作数时,情况会稍微复杂一些。在迭代开始之前,任何归约操作数都必须初始化为其起始值。下面我们演示如何做到这一点,沿 a 的最后一个轴进行求和。
示例
>>> import numpy as np
>>> a = np.arange(24).reshape(2,3,4)
>>> it = np.nditer([a, None], flags=['reduce_ok'],
... op_flags=[['readonly'], ['readwrite', 'allocate']],
... op_axes=[None, [0,1,-1]])
>>> with it:
... it.operands[1][...] = 0
... for x, y in it:
... y[...] += x
... result = it.operands[1]
...
>>> result
array([[ 6, 22, 38],
[54, 70, 86]])
>>> np.sum(a, axis=2)
array([[ 6, 22, 38],
[54, 70, 86]])
要进行缓冲归约,在设置过程中还需要进行另一项调整。通常,迭代器构造涉及将可读数组中的第一个数据缓冲区复制到缓冲区中。任何归约操作数都是可读的,因此可以将其读入缓冲区。不幸的是,在此缓冲操作完成后对操作数的初始化将不会反映在迭代开始时的缓冲区中,并且会产生垃圾结果。
迭代器标志“delay_bufalloc”用于允许迭代器分配的归约操作数与缓冲同时存在。当设置此标志时,迭代器将使其缓冲区保持未初始化状态,直到收到重置,之后它将准备好进行常规迭代。如果我们也启用缓冲,那么前面的示例将如下所示。
示例
>>> import numpy as np
>>> a = np.arange(24).reshape(2,3,4)
>>> it = np.nditer([a, None], flags=['reduce_ok',
... 'buffered', 'delay_bufalloc'],
... op_flags=[['readonly'], ['readwrite', 'allocate']],
... op_axes=[None, [0,1,-1]])
>>> with it:
... it.operands[1][...] = 0
... it.reset()
... for x, y in it:
... y[...] += x
... result = it.operands[1]
...
>>> result
array([[ 6, 22, 38],
[54, 70, 86]])
将内部循环放入 Cython#
那些希望从底层操作中获得真正良好性能的人应该强烈考虑直接使用 C 中提供的迭代 API,但对于那些不熟悉 C 或 C++ 的人来说,Cython 是一个不错的折衷方案,具有合理的性能权衡。对于 nditer
对象,这意味着让迭代器处理广播、dtype 转换和缓冲,同时将内部循环交给 Cython。
在我们的示例中,我们将创建一个平方和函数。首先,让我们用简单的 Python 实现这个函数。我们希望支持类似于 numpy sum
函数的 'axis' 参数,因此我们需要为 op_axes 参数构建一个列表。下面是它的样子。
示例
>>> def axis_to_axeslist(axis, ndim):
... if axis is None:
... return [-1] * ndim
... else:
... if type(axis) is not tuple:
... axis = (axis,)
... axeslist = [1] * ndim
... for i in axis:
... axeslist[i] = -1
... ax = 0
... for i in range(ndim):
... if axeslist[i] != -1:
... axeslist[i] = ax
... ax += 1
... return axeslist
...
>>> def sum_squares_py(arr, axis=None, out=None):
... axeslist = axis_to_axeslist(axis, arr.ndim)
... it = np.nditer([arr, out], flags=['reduce_ok',
... 'buffered', 'delay_bufalloc'],
... op_flags=[['readonly'], ['readwrite', 'allocate']],
... op_axes=[None, axeslist],
... op_dtypes=['float64', 'float64'])
... with it:
... it.operands[1][...] = 0
... it.reset()
... for x, y in it:
... y[...] += x*x
... return it.operands[1]
...
>>> a = np.arange(6).reshape(2,3)
>>> sum_squares_py(a)
array(55.)
>>> sum_squares_py(a, axis=-1)
array([ 5., 50.])
要将此函数 Cython 化,我们将内部循环 (y[...] += x*x) 替换为专门用于 float64 dtype 的 Cython 代码。启用 'external_loop' 标志后,提供给内部循环的数组将始终是一维的,因此几乎不需要进行检查。
以下是 sum_squares.pyx 的代码列表
import numpy as np
cimport numpy as np
cimport cython
def axis_to_axeslist(axis, ndim):
if axis is None:
return [-1] * ndim
else:
if type(axis) is not tuple:
axis = (axis,)
axeslist = [1] * ndim
for i in axis:
axeslist[i] = -1
ax = 0
for i in range(ndim):
if axeslist[i] != -1:
axeslist[i] = ax
ax += 1
return axeslist
@cython.boundscheck(False)
def sum_squares_cy(arr, axis=None, out=None):
cdef np.ndarray[double] x
cdef np.ndarray[double] y
cdef int size
cdef double value
axeslist = axis_to_axeslist(axis, arr.ndim)
it = np.nditer([arr, out], flags=['reduce_ok', 'external_loop',
'buffered', 'delay_bufalloc'],
op_flags=[['readonly'], ['readwrite', 'allocate']],
op_axes=[None, axeslist],
op_dtypes=['float64', 'float64'])
with it:
it.operands[1][...] = 0
it.reset()
for xarr, yarr in it:
x = xarr
y = yarr
size = x.shape[0]
for i in range(size):
value = x[i]
y[i] = y[i] + value * value
return it.operands[1]
在这台机器上,将 .pyx 文件构建为模块的过程如下所示,但您可能需要查阅一些 Cython 教程以了解适合您系统配置的具体信息。
$ cython sum_squares.pyx
$ gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -I/usr/include/python2.7 -fno-strict-aliasing -o sum_squares.so sum_squares.c
从 Python 解释器运行此代码会产生与我们的原生 Python/NumPy 代码相同的结果。
示例
>>> from sum_squares import sum_squares_cy
>>> a = np.arange(6).reshape(2,3)
>>> sum_squares_cy(a)
array(55.0)
>>> sum_squares_cy(a, axis=-1)
array([ 5., 50.])
在 IPython 中进行一些计时表明,Cython 内部循环减少了开销和内存分配,相对于直接的 Python 代码和使用 NumPy 内置 sum 函数的表达式,提供了非常显著的速度提升。
>>> a = np.random.rand(1000,1000)
>>> timeit sum_squares_py(a, axis=-1)
10 loops, best of 3: 37.1 ms per loop
>>> timeit np.sum(a*a, axis=-1)
10 loops, best of 3: 20.9 ms per loop
>>> timeit sum_squares_cy(a, axis=-1)
100 loops, best of 3: 11.8 ms per loop
>>> np.all(sum_squares_cy(a, axis=-1) == np.sum(a*a, axis=-1))
True
>>> np.all(sum_squares_py(a, axis=-1) == np.sum(a*a, axis=-1))
True