Estructura de un ndarray
#
Es interesante conocer a grandes rasgos cómo se organiza la estructura interna de un objeto ndarray
El objeto contiene básicamente un buffer con datos en la memoria, más la información sobre cómo interpretar el contenido de la memoria más la información sobre cómo moverse dentro de la memoria asignada.
Aparte de estos atributos, podemos acceder desde Python (aunque rara vez es necesario)
import numpy as np
x = np.array([3, 6, -1])
print(x.data)
bytes(x.data)
<memory at 0x7fe9503bb1c0>
b'\x03\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff'
Podemos ver una representación en Python de la estructura interna accediendo a __array_interface__
x.__array_interface__
{'data': (94559977441856, False),
'strides': None,
'descr': [('', '<i8')],
'typestr': '<i8',
'shape': (3,),
'version': 3}
Esta estructura contiene la información necesaria para interpretar el fragmento de memoria
del array. En typestr
tenemos la información necesaria para interpretar los datos
y stride
indica cuánto hay que desplazarse para acceder al siguiente elemento. None
indica un array de C contiguo en memoria.
Los datos del array pueden ser compartidos por diversos objetos o incluso estar definido de manera externa, por ejemplo
# Bytes
bf = b'1234'
y = np.frombuffer(bf, dtype=np.int8)
y.base is bf
True
También hay información en el atributo flags
y.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : False
ALIGNED : True
WRITEBACKIFCOPY : False
La estructura nos indica que los datos no pertenecen al array
Otro ejemplo, con una sección:
x = np.ones((4,4))
y = x[1:3,0:2]
y.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
Aquí tenemos que los datos no pertenecen a y
(OWNDATA : False),
pero que podemos
escribir en la variable y
(WRITEABLE : True
); además
los cambios de y
se propagan a x
.
Atributos y métodos del array#
Hemos visto algunos atributos, aunque en las manipulaciones normales rara
vez se usan. Veremos ahora atributos más interesantes. Para ello definimos
un array de tres dimensiones. Los tres primeros atributos son el ńumero
de dimensiones ndim
, la forma shape
y el tamaño (número de elementos) size
.
x3 = np.ones((3, 4, 5))
print('x3.ndim=', x3.ndim)
print('x3.shape=', x3.shape)
print('x3.size=', x3.size)
x3.ndim= 3
x3.shape= (3, 4, 5)
x3.size= 60
Otro atributo importante es dtype
, del que hablaremos en la siguiente sección.
Descriptores de datos#
Hemos visto que numpy utiliza una estructura dtype
para describir los datos de un array. Esta estructura contiene
la clase de los datos type
, el tamaño del bloque de datos en
bytes itemsize
, el orden de los bytes (big-endian >, little-endian <
, el del sistema = o no aplicable |) en byteorder
.
tp = np.dtype(int)
tp = np.dtype('uint16')
print('type', tp.type)
print('itemsize', tp.itemsize)
print('byteorder', tp.byteorder)
type <class 'numpy.uint16'>
itemsize 2
byteorder =
La lista de tipos disponibles puede encontrarse en la documentacion de Numpy
Conversión de tipos#
Se pueden realizar conversiones de tipo manualmente (con el método astype
)
automáticamente en ufuncs y en asignaciones. El casting se basa en los tipos
de datos de los arrays, no en su contenido.
Veamos algunos ejemplos
x = np.array([1, 2, 3, 4], dtype=np.float32)
print(x)
y = x.astype(np.int8)
print(repr(y))
[1. 2. 3. 4.]
array([1, 2, 3, 4], dtype=int8)
En una operación de suma se está usando internamente ufuncs. Se realiza conversión de tipos basado en el tipo del array.
Para ver las reglas en detalle, de nuevo podemos acudir a la documentación de numpy
Vistas#
En las conversiones de datos, cambiamos los datos existentes a un nuevo tipo. En una vista, cambiamos el dtype sin cambiar los datos subyacentes.
Por ejemplo, un array con 4 uint8 puede reinterpretarse como 4 int8 o también 2 int16 o 1 int32 o 1 float32.
Podemos forzar un cambio de dtype:
x = np.array([1, 2, 3, 4], dtype=np.uint8)
x.dtype = '=i2'
print(repr(x))
array([ 513, 1027], dtype=int16)
O bien crear una nueva vista
x = np.array([1, 2, 3, 4], dtype=np.uint8)
y = x.view('=i4')
print(repr(y))
y.flags
array([67305985], dtype=int32)
C_CONTIGUOUS : True
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
Cómo funcionan los índices: strides#
¿Cómo podemos acceder a los elementos del array?
x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.int8)
x.tobytes('A')
b'\x01\x02\x03\x04\x05\x06\x07\x08\t'
¿En qué byte del buffer interno de x
empieza x[1,2]?
Se almacena en un atributo llamado strides
. Indica
cuántos bytes hay que moverse para alcanzar un cierto elemento.
print(x.strides)
byte_offset = 3*1 + 1*2
print(x.flat[byte_offset])
print(x[1,2])
(3, 1)
6
6
strides
combina el tamaño del array dado con shape
con el tamaño
del tipo de dato en dtype.
Parte de las operaciones de indexación y algunas otras como la trasposición
se puede obtener simplemente cambiando strides
x = np.array([1, 2, 3, 4, 5, 6], dtype=np.int32)
print('x.strides=',x.strides)
y = x[::-1]
print('y=',y)
print('y.strides=',y.strides)
x.strides= (4,)
y= [6 5 4 3 2 1]
y.strides= (-4,)
x = np.zeros((10,10,10), dtype=np.int32)
print('x.strides=',x.strides)
y = x.T
print('y.strides=',y.strides)
x.strides= (400, 40, 4)
y.strides= (4, 40, 400)
Se puede manipular los valores de stride
de un array para conseguir
manipulaciones avanzadas, como extraer diagonales, repetir elementos, etc.
Pueden verse ejemplos aquí