¿Cómo se implementa la indexación de lujo de Numpy?

Estaba haciendo un poco de experimentación con listas 2D y matrices numpy. A partir de esto, he planteado 3 preguntas por las que siento curiosidad por saber la respuesta.

Primero, inicialicé una lista de python 2D.

>>> my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 

Luego traté de indexar la lista con una tupla.

 >>> my_list[:,] Traceback (most recent call last): File "", line 1, in  TypeError: list indices must be integers, not tuple 

Ya que el intérprete me lanza un TypeError y no un SyntaxError , supongo que en realidad es posible hacerlo, pero Python no lo admite de forma nativa.

Luego intenté convertir la lista a una matriz numpy y hacer lo mismo.

 >>> np.array(my_list)[:,] array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) 

Por supuesto no hay problema. Tengo entendido que uno de los __xx__() se ha invalidado e implementado en el paquete numpy .

La indexación de Numpy también soporta listas:

 >>> np.array(my_list)[:,[0, 1]] array([[1, 2], [4, 5], [7, 8]]) 

Esto ha planteado un par de preguntas:

  1. ¿ __xx__ método __xx__ tiene un número reemplazado / definido para manejar la indexación sofisticada?
  2. ¿Por qué las listas de python no admiten nativamente la indexación de fantasía?

(Pregunta extra: ¿por qué mis tiempos muestran que cortar en python2 es más lento que en python3?)

Tienes tres preguntas:

1. ¿ __xx__ método __xx__ ha sido invalidado / definido para manejar la indexación de fantasía?

El operador de indexación [] se puede anular utilizando __getitem__ , __setitem__ y __delitem__ . Puede ser divertido escribir una subclase simple que ofrezca alguna introspección:

 >>> class VerboseList(list): ... def __getitem__(self, key): ... print(key) ... return super().__getitem__(key) ... 

Hagamos uno vacío primero:

 >>> l = VerboseList() 

Ahora llénalo con algunos valores. Tenga en cuenta que no hemos anulado __setitem__ por lo que todavía no ocurre nada interesante:

 >>> l[:] = range(10) 

Ahora vamos a obtener un elemento. En el índice 0 será 0 :

 >>> l[0] 0 0 

Si intentamos usar una tupla, obtenemos un error, ¡pero primero podemos ver la tupla!

 >>> l[0, 4] (0, 4) Traceback (most recent call last): File "", line 1, in  File "", line 4, in __getitem__ TypeError: list indices must be integers or slices, not tuple 

También podemos descubrir cómo Python representa rebanadas internamente:

 >>> l[1:3] slice(1, 3, None) [1, 2] 

Hay muchas más cosas divertidas que puedes hacer con este objeto. ¡Pruébalo!

2. ¿Por qué las listas de python no admiten nativamente la indexación de fantasía?

Esto es difícil de responder. Una forma de pensar acerca de esto es histórica: porque los desarrolladores de numpy pensaron primero.

Ustedes jóvenes Cuando yo era un niño…

En su primer lanzamiento público en 1991, Python no tenía una biblioteca numpy , y para hacer una lista multidimensional, tenía que anidar estructuras de listas. Supongo que los primeros desarrolladores, en particular, Guido van Rossum ( GvR ), sintieron que inicialmente era mejor mantener las cosas simples. La indexación de rebanadas ya era bastante poderosa.

Sin embargo, no mucho después, creció el interés en usar Python como un lenguaje de computación científica. Entre 1995 y 1997, varios desarrolladores colaboraron en una biblioteca llamada numpy , uno de los primeros predecesores de numpy . A pesar de que no fue un colaborador importante en numeric o numpy , GvR se coordinó con los desarrolladores numeric , ampliando la syntax de Python de una manera que facilitó la indexación de matrices multidimensional. Más tarde, surgió una alternativa a la numeric llamada numarray ; y en 2006, se creó numpy , incorporando las mejores características de ambos.

Estas bibliotecas eran poderosas, pero requerían extensas extensiones de c, etc. Trabajarlos en la distribución de Python base lo habría hecho voluminoso. Y aunque GvR mejoró un poco la syntax de los segmentos, agregar un índice sofisticado a las listas ordinarias habría cambiado su API dramáticamente, y de forma algo redundante. Dado que ya se podía tener una indexación elegante con una biblioteca externa, el beneficio no valía la pena.

Partes de esta narrativa son especulativas, con toda honestidad. 1 ¡No conozco a los desarrolladores realmente! Pero es la misma decisión que habría tomado. De hecho…

Realmente debería ser así.

Aunque la indexación elegante es muy poderosa, me alegro de que no sea parte de Python de vainilla incluso hoy en día, porque significa que no tienes que pensar mucho cuando trabajas con listas comunes. Para muchas tareas no lo necesita, y la carga cognitiva que impone es significativa.

Tenga en cuenta que estoy hablando de la carga impuesta a los lectores y mantenedores . Es posible que seas un genio de la magia que puede hacer productos de tensor en 5-d en tu cabeza, pero otras personas tienen que leer tu código. Mantener la indexación elegante en numpy significa que las personas no la usan a menos que la necesiten honestamente, lo que hace que el código sea más legible y mantenible en general.

3. ¿Por qué es tan lenta la indexación elegante de numpy en python2? ¿Es porque no tengo soporte BLAS nativo para numpy en esta versión?

Posiblemente. Definitivamente es dependiente del medio ambiente; No veo la misma diferencia en mi máquina.


1. Las partes de la narrativa que no son tan especulativas se extraen de una breve historia contada en una edición especial de Computing in Science and Engineering (2011 vol. 13).

my_list[:,] es traducido por el intérprete a

 my_list.__getitem__((slice(None, None, None),)) 

Es como llamar a una función con *args , pero se encarga de traducir la notación : en un objeto de slice . Sin el , simplemente pasaría la slice . Con el , pasa una tupla.

La lista __getitem__ no acepta una tupla, como se muestra en el error. Una matriz __getitem__ hace. Creo que la capacidad de pasar una tupla y crear objetos de sector se agregó como conveniencia para numpy (o sus predictores). La notación de la tupla nunca se ha agregado a la lista __getitem__ . (Hay una clase operator.itemgetter que permite una forma de indexación avanzada, pero internamente es solo un iterador de código Python).

Con una matriz puedes usar la notación de la tupla directamente:

 In [490]: np.arange(6).reshape((2,3))[:,[0,1]] Out[490]: array([[0, 1], [3, 4]]) In [491]: np.arange(6).reshape((2,3))[(slice(None),[0,1])] Out[491]: array([[0, 1], [3, 4]]) In [492]: np.arange(6).reshape((2,3)).__getitem__((slice(None),[0,1])) Out[492]: array([[0, 1], [3, 4]]) 

Mire el numpy/lib/index_tricks.py , por ejemplo, cosas divertidas que puede hacer con __getitem__ . Puedes ver el archivo con

 np.source(np.lib.index_tricks) 

Una lista anidada es una lista de listas:

En una lista anidada, las listas secundarias son independientes de la lista que contiene. El contenedor solo tiene punteros a otros objetos en la memoria:

 In [494]: my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] In [495]: my_list Out[495]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] In [496]: len(my_list) Out[496]: 3 In [497]: my_list[1] Out[497]: [4, 5, 6] In [498]: type(my_list[1]) Out[498]: list In [499]: my_list[1]='astring' In [500]: my_list Out[500]: [[1, 2, 3], 'astring', [7, 8, 9]] 

Aquí cambio el segundo elemento de my_list ; ya no es una lista, sino una cadena.

Si aplico [:] a una lista, solo obtengo una copia superficial:

 In [501]: xlist = my_list[:] In [502]: xlist[1] = 43 In [503]: my_list # didn't change my_list Out[503]: [[1, 2, 3], 'astring', [7, 8, 9]] In [504]: xlist Out[504]: [[1, 2, 3], 43, [7, 8, 9]] 

pero cambiar un elemento de una lista en xlist cambia la lista secundaria correspondiente en my_list :

 In [505]: xlist[0][1]=43 In [506]: my_list Out[506]: [[1, 43, 3], 'astring', [7, 8, 9]] 

Para mí, esto se muestra mediante la indexación n-dimensional (como se implementó para matrices numpy) no tiene sentido con listas anidadas. Las listas anidadas son multidimensionales solo en la medida en que lo permitan sus contenidos; No hay nada estructural o sintácticamente multidimensional en ellos.

los tiempos

El uso de dos [:] en una lista no hace una copia en profundidad ni se desplaza hacia el nested. Simplemente repite el paso de copia superficial:

 In [507]: ylist=my_list[:][:] In [508]: ylist[0][1]='boo' In [509]: xlist Out[509]: [[1, 'boo', 3], 43, [7, 8, 9]] 

arr[:,] solo hace una view de arr . La diferencia entre la view y la copy es parte de la comprensión de la diferencia entre la indexación básica y avanzada.

Así que alist[:][:] y arr[:,] son formas diferentes, pero básicas de hacer algún tipo de copia de listas y matrices. Ninguno calcula nada, y tampoco itera a través de los elementos. Así que una comparación de tiempo no nos dice mucho.

¿ __xx__ método __xx__ tiene un número reemplazado / definido para manejar la indexación sofisticada?

__getitem__ para recuperación, __setitem__ para asignación. Sería __delitem__ para su eliminación, excepto que las matrices NumPy no admiten la eliminación.

(Sin embargo, todo está escrito en C, por lo que lo que implementaron a nivel C fue mp_subscript y mp_ass_subscript , y __getitem__ y __setitem__ wrappers fueron proporcionados por PyType_Ready . __delitem__ PyType_Ready . nivel.)

¿Por qué las listas de python no admiten nativamente la indexación de fantasía?

Las listas de Python son fundamentalmente estructuras unidimensionales, mientras que las matrices NumPy son de dimensiones arbitrarias. La indexación multidimensional solo tiene sentido para estructuras de datos multidimensionales.

Puede tener una lista con listas como elementos, como [[1, 2], [3, 4]] , pero la lista no sabe o se preocupa por la estructura de sus elementos. Hacer que las listas admitan l[:, 2] indexación requeriría que la lista fuera consciente de la estructura multidimensional de una manera que las listas no están diseñadas para ser. También agregaría mucha complejidad, mucho manejo de errores y muchas decisiones de diseño adicionales. ¿Qué tan profunda debe ser una copia l[:, :] ? ¿Qué sucede si la estructura es irregular o está anidada de manera inconsistente? ¿Debería la indización multidimensional retroceder en elementos no listados? ¿Qué del l[1:3, 1:3] haría?

He visto la implementación de indexación NumPy, y es más larga que toda la implementación de listas. Aquí hay parte de ello. No vale la pena hacer eso en las listas cuando los arreglos NumPy satisfacen todos los casos de uso realmente convincentes para los que lo necesitarías.

¿Por qué es tan lenta la indexación elegante de numpy en python2? ¿Es porque no tengo soporte BLAS nativo para numpy en esta versión?

La indexación NumPy no es una operación BLAS, así que no es así. No puedo reproducir diferencias de tiempo tan dramáticas, y las diferencias que veo parecen ser optimizaciones menores de Python 3, quizás una asignación un poco más eficiente de tuplas o cortes. Lo que estás viendo es probablemente debido a las diferencias de versión NumPy.