nditer
NumPy 1.6中引入的迭代器对象提供了许多灵活的方式来系统地访问一个或多个数组的所有元素。本页介绍了一些基本的方法来使用该对象在Python中对数组进行计算,然后总结如何在Cython中加速内部循环。由于Python的公开
nditer
是C数组迭代器API的相对直接的映射,因此这些想法也将为使用C或C ++进行数组迭代提供帮助。
可以完成的最基本的任务nditer
是访问数组的每个元素。使用标准的Python迭代器接口逐一提供每个元素。
例
>>> 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顺序获取该转置的副本相比,我们可以通过迭代上一个数组的转置来看到这一点。
例
>>> 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和aT的元素以相同的顺序遍历,即它们在内存中的存储顺序,而aTcopy(order ='C')的元素 以不同的顺序访问,因为它们已放入不同的内存中布局。
在某些时候,重要的是要以特定顺序访问数组的元素,而不管元素在内存中的布局如何。该nditer
对象提供了一个顺序参数来控制迭代的这一方面。具有上述行为的默认值是order ='K',以保留现有订单。对于C订单,可以使用order ='C'覆盖;对于Fortran订单,可以使用order ='F'覆盖。
例
>>> 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在迭代完成后必须将此缓冲区数据复制回原始数组,因此您必须使用两种方法之一通知结束迭代的时间。您可以:
使用with语句将nditer用作上下文管理器,退出上下文时将写回临时数据。
迭代完成后,调用迭代器的close方法,这将触发回写。
一旦调用close或退出其上下文,nditer将无法再进行迭代。
例
>>> 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顺序时,它必须提供三个块,每个块包含两个元素。
例
>>> 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属性进行访问。以下示例显示了显示索引进度的打印输出:
例
>>> 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
对象将引发异常。
例
>>> 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
提供了另一种用于迭代的语法,该语法可与迭代器对象本身明确地协同工作。使用这种循环结构,可以通过索引迭代器来访问当前值。其他属性(如跟踪索引)保持不变。下面的示例产生与上一节相同的结果。
例
>>> 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=' ')
... 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=' ')
... 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]
... it.iternext()
...
>>> a
array([[ 0, 1, 2],
[-1, 0, 1]])
在强制执行迭代顺序时,我们观察到外部循环选项可能会以较小的块提供元素,因为无法以恒定的步幅按适当的顺序访问元素。在编写C代码时,这通常很好,但是在纯Python代码中,这可能会导致性能显着降低。
通过启用缓冲模式,可以使迭代器提供给内部循环的块更大,从而显着减少Python解释器的开销。在强制Fortran迭代顺序的示例中,启用缓冲后,内部循环可以一次性查看所有元素。
例
>>> 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代码时,通常最好让迭代器处理复制或缓冲,而不要自己在内部循环中强制转换数据类型。
有两种机制可以做到这一点,即临时副本和缓冲模式。对于临时副本,将使用新的数据类型创建整个数组的副本,然后在该副本中进行迭代。通过所有迭代完成后更新原始数组的模式,可以进行写访问。临时副本的主要缺点是,临时副本可能会消耗大量内存,尤其是如果迭代数据类型的项目大小大于原始项目的大小。
缓冲模式可减轻内存使用问题,并且比制作临时副本更具缓存友好性。除特殊情况外,在迭代器外需要一次整个数组的情况下,建议在临时复制上使用缓冲。在NumPy中,ufunc和其他函数使用缓冲来支持灵活的输入,同时将内存开销降至最低。
在我们的示例中,我们将使用复杂的数据类型来处理输入数组,以便我们可以取负数的平方根。如果不启用复制或缓冲模式,则迭代器将在数据类型不完全匹配时引发异常。
例
>>> 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
在复制模式下,“复制”被指定为每个操作数标志。这样做是为了以每个操作数的方式提供控制。缓冲模式被指定为迭代器标志。
例
>>> 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.73205080757j 1.41421356237j 1j 0j (1+0j) (1.41421356237+0j)
>>> for x in np.nditer(a, flags=['buffered'], op_dtypes=['complex128']):
... print(np.sqrt(x), end=' ')
...
1.73205080757j 1.41421356237j 1j 0j (1+0j) (1.41421356237+0j)
迭代器使用NumPy的转换规则来确定是否允许特定的转换。默认情况下,它强制执行“安全”强制转换。例如,这意味着,如果您尝试将64位浮点数组视为32位浮点数组,它将引发异常。在许多情况下,规则“ same_kind”是最合理使用的规则,因为它将允许从64位浮点数转换为32位浮点数,但不允许从float转换为int或从复数转换为float。
例
>>> 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'强制转换来允许处理其他浮点类型。在只读模式下,可以提供一个整数数组,而读写模式将引发异常,因为转换回该数组将违反强制转换规则。
例
>>> 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
当您需要编写这样的函数时,对象可以为您应用这些规则。
例如,我们打印出一起广播一维和二维数组的结果。
例
>>> 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
发生广播错误时,迭代器会引发一个异常,其中包括有助于诊断问题的输入形状。
例
>>> 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):
File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (2) (2,3)
NumPy函数的一个常见情况是根据输入的广播分配输出,并另外具有一个称为“ out”的可选参数,在提供结果时将在其中放置结果。该nditer
对象提供了一个方便的习惯用法,可以非常轻松地支持此机制。
我们将通过创建一个square
平方输入的函数来演示其工作原理。让我们从不包含“ out”参数支持的最小功能定义开始。
例
>>> 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',并且我们的内部循环将失败。输入数组默认为“只读”的原因是为了避免因意外触发归约操作而引起的混乱。如果默认值为“ readwrite”,则任何广播操作都将触发减少操作,该主题将在本文档后面介绍。
在讨论的同时,我们还引入了'no_broadcast'标志,该标志将防止广播输出。这很重要,因为我们只希望每个输出一个输入值。汇总多个输入值是一种减少操作,需要特殊处理。因为已经必须在迭代器标志中显式启用减少功能,所以它已经引发了错误,但是对于最终用户而言,禁用广播所导致的错误消息更加容易理解。若要查看如何将平方函数归纳为约简,请参见有关Cython的部分中的平方和函数。
为了完整起见,我们还将添加“ external_loop”和“ buffered”标志,因为出于性能原因通常需要这些标志。
例
>>> 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):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in square
ValueError: non-broadcastable output operand with shape (3) doesn't match the broadcast shape (2,3)
任何二进制运算都可以像in中那样以外部乘积方式扩展为数组运算outer
,并且该nditer
对象提供了一种通过显式映射操作数的轴来实现此目的的方法。也可以通过newaxis
索引来做到这一点,但是我们将向您展示如何直接使用nditer op_axes
参数来完成此过程,而无需任何中间视图。
我们将做一个简单的外部乘积,将第一个操作数的尺寸放在第二个操作数的尺寸之前。所述op_axes 参数需要轴中的一个列表中的每个操作数,并从迭代器的轴来的操作数的轴提供了一个映射。
假设第一个操作数是一维的,第二个操作数是二维的。迭代器将具有三个维度,因此op_axes 将具有两个3元素列表。第一个列表选择第一个操作数的一个轴,其余迭代器轴为-1,最终结果为[0,-1,-1]。第二个列表选择第二个操作数的两个轴,但不应与第一个操作数中选择的轴重叠。它的列表是[-1,0,1]。输出操作数以标准方式映射到迭代器轴,因此我们可以提供None而不是构造另一个列表。
内循环中的运算是简单的乘法。与外部产品有关的所有事情都由迭代器设置处理。
例
>>> 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
并且必须使用在上下文管理器中创建的引用。