python - usar - que es numpy
¿Cómo se implementa la indexación de lujo de Numpy? (3)
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 "<stdin>", line 1, in <module>
TypeError: list indices must be integers, not tuple
Como 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 traté de 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:
- ¿
__xx__
método__xx__
tiene un número reemplazado / definido para manejar la indexación elegante? - ¿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?)
¿
__xx__
método__xx__
tiene un número reemplazado / definido para manejar la indexación elegante?
__getitem__
para recuperación, __setitem__
para asignación. Sería __delitem__
para la eliminación, excepto que las matrices NumPy no admiten la eliminación.
(Sin embargo, todo está escrito en C, de modo que lo que implementaron en el 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 tenga en cuenta 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é haría del l[1:3, 1:3]
?
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 can''t reproduce 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.
Tienes tres preguntas:
1. ¿ __xx__
método __xx__
ha reemplazado / definido numpy para manejar la indexación sofisticada?
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 "<stdin>", line 1, in <module>
File "<stdin>", 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 lista. 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 sintaxis 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 c, etc. Trabajarlos en la distribución de Python base lo habría hecho voluminoso. Y aunque GvR mejoró un poco la sintaxis 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 de fantasía 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 fácil de leer y mantener 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
para ver ejemplos de 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 el cambio de un elemento de una lista en xlist
cambia la sublista 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 profunda ni se desplaza hacia abajo en el anidamiento. 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.