6.5. Forma y pasos¶
Los datos dentro de un ndarray son un único bloque compacto de números. El descriptor que precede a ese bloque decide cómo se lee ese bloque plano como un tensor.
6.5.1. Lo que registra el descriptor¶
Cinco valores describen cómo leer el bloque de datos como un tensor:
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)
a.ndim # 2 - number of dimensions
a.shape # (2, 3)- length along each dimension
a.itemsize # 1 - bytes per element (from dtype)
a.size # 6 - total number of elements
a.strides # (3, 1)- step pattern through the buffer
El ayudante ndinfo() imprime todos ellos más la ubicación del búfer subyacente en una sola llamada. Dos arreglos cuyas ubicaciones de búfer coinciden están compartiendo memoria:
np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8
6.5.2. Los pasos explicados¶
Un paso (stride) es la cantidad de bytes que hay que avanzar en el bloque de datos para moverse un elemento a lo largo de un eje dado. Para el arreglo uint8 de 2x3 anterior, los pasos son (3, 1): bajar una fila salta 3 bytes, moverse una columna a la derecha salta 1 byte. Eso equivale a decir que las filas se almacenan una tras otra, de izquierda a derecha:
memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
^ row 0 ^ row 1
<------- 3 bytes ---->
Para leer a[i, j], numpy calcula i * strides[0] + j * strides[1] desde el inicio del bloque de datos y lee itemsize bytes a partir de ahí. La misma fórmula se extiende a cualquier número de dimensiones.
Esta disposición – filas almacenadas una tras otra, con el último eje variando más rápido a lo largo de la memoria – se denomina orden row-major (por filas). Cada arreglo que numpy asigna en la cámara usa esta disposición.
6.5.3. El orden por filas tiene consecuencias¶
Del hecho de que «las filas se almacenan una tras otra» se derivan dos cosas que importan al dar forma a un búfer en la cámara.
El último eje es contiguo. Recorrer de a[0, 0] a a[0, 1] toca el byte siguiente. Recorrer de a[0, 0] a a[1, 0] salta a través de toda una fila.
El último eje es el eje rápido para el cálculo sobre todo el arreglo. numpy en la cámara siempre recorre el último eje en el nivel más interno, independientemente de cuál eje resulte ser más largo. La biblioteca numpy de escritorio reordena silenciosamente sus bucles para poner el eje más largo en el nivel más interno; la cámara no lo hace, por lo que una elección de disposición que numpy de escritorio habría disimulado todavía cuesta tiempo aquí. np.sum(m, axis=1) colapsa el último eje y se ejecuta en la dirección contigua; np.sum(m, axis=0) no. Cuando la aplicación puede elegir cómo disponer un búfer, pon el eje largo al final para que las operaciones a lo largo de él permanezcan en el bucle interno.
Si la disposición empieza siendo incorrecta, transpose() (o el atajo .T) la corrige sin copiar los datos – simplemente intercambia los pasos:
a = b.T # now iterates fast
Rendimiento tiene la discusión completa sobre rendimiento.
6.5.4. Reshape, transpose, rebanado – ediciones del descriptor¶
Cualquier operación que solo reescribe el descriptor es gratuita. reshape intercambia una nueva shape y unos nuevos strides sobre el mismo bloque de datos. transpose invierte los pasos. a[::2] duplica un paso. Cada una devuelve una vista del mismo búfer subyacente.
Cualquier cosa que tenga que recorrer los datos y escribir un búfer nuevo es una copia. La regla por ahora es que las ediciones del descriptor son gratuitas y los recorridos de datos no.
6.5.5. Una nota sobre ndim¶
numpy en la cámara está compilado con un ndim máximo admitido de 4. Las operaciones que producirían un arreglo de rango superior generan un ValueError. La gran mayoría del trabajo del lado de la cámara es de 1-D o 2-D, por lo que el límite rara vez es un problema.