español - ¿Cómo puedo, en Python, iterar sobre múltiples listas 2d a la vez, limpiamente?
plotly title (10)
¿Estás seguro de que los objetos en las dos matrices que estás iterando en paralelo son instancias de clases conceptualmente distintas? ¿Qué hay de combinar las dos clases que terminan con una matriz de objetos que contienen tanto IsWhatever () como doSomething ()?
Si estoy haciendo un juego simple basado en cuadrículas, por ejemplo, podría tener algunas listas en 2d. Uno podría ser para terreno, otro podría ser para objetos, etc. Desafortunadamente, cuando necesito iterar sobre las listas y hacer que el contenido de un cuadrado en una lista afecte parte de otra lista, tengo que hacer algo como esto.
for i in range(len(alist)):
for j in range(len(alist[i])):
if alist[i][j].isWhatever:
blist[i][j].doSomething()
¿Hay alguna manera más agradable de hacer algo como esto?
Cuando está trabajando con cuadrículas de números y desea un rendimiento realmente bueno, debería considerar usar Numpy . Es sorprendentemente fácil de usar y le permite pensar en términos de operaciones con grillas en lugar de bucles sobre grillas. El rendimiento proviene del hecho de que las operaciones se ejecutan en redes completas con código SSE optimizado.
Por ejemplo, aquí hay algunos numpy que usan el código que escribí que hace la fuerza bruta de simulación numérica de partículas cargadas conectadas por resortes. Este código calcula un intervalo de tiempo para un sistema 3D con 100 nodos y 99 bordes en 31 ms. Eso es más de 10 veces más rápido que el mejor código de pitón puro que pude encontrar.
from numpy import array, sqrt, float32, newaxis
def evolve(points, velocities, edges, timestep=0.01, charge=0.1, mass=1., edgelen=0.5, dampen=0.95):
"""Evolve a n body system of electrostatically repulsive nodes connected by
springs by one timestep."""
velocities *= dampen
# calculate matrix of distance vectors between all points and their lengths squared
dists = array([[p2 - p1 for p2 in points] for p1 in points])
l_2 = (dists*dists).sum(axis=2)
# make the diagonal 1''s to avoid division by zero
for i in xrange(points.shape[0]):
l_2[i,i] = 1
l_2_inv = 1/l_2
l_3_inv = l_2_inv*sqrt(l_2_inv)
# repulsive force: distance vectors divided by length cubed, summed and multiplied by scale
scale = timestep*charge*charge/mass
velocities -= scale*(l_3_inv[:,:,newaxis].repeat(points.shape[1], axis=2)*dists).sum(axis=1)
# calculate spring contributions for each point
for idx, (point, outedges) in enumerate(izip(points, edges)):
edgevecs = point - points.take(outedges, axis=0)
edgevec_lens = sqrt((edgevecs*edgevecs).sum(axis=1))
scale = timestep/mass
velocities[idx] += (edgevecs*((((edgelen*scale)/edgevec_lens - scale))[:,newaxis].repeat(points.shape[1],axis=1))).sum(axis=0)
# move points to new positions
points += velocities*timestep
Comenzaría escribiendo un método generador:
def grid_objects(alist, blist):
for i in range(len(alist)):
for j in range(len(alist[i])):
yield(alist[i][j], blist[i][j])
Luego, cuando necesite repetir las listas, su código se verá así:
for (a, b) in grid_objects(alist, blist):
if a.is_whatever():
b.do_something()
Como un ligero cambio de estilo, puede usar enumerate:
for i, arow in enumerate(alist):
for j, aval in enumerate(arow):
if aval.isWhatever():
blist[i][j].doSomething()
No creo que obtenga nada significativamente más simple a menos que reordene sus estructuras de datos como sugiere Federico. Para que puedas convertir la última línea en algo así como "aval.b.doSomething ()".
Las expresiones de generador y el izip del módulo itertools funcionarán muy bien aquí:
from itertools import izip
for a, b in (pair for (aline, bline) in izip(alist, blist)
for pair in izip(aline, bline)):
if a.isWhatever:
b.doSomething()
La línea for
declaración anterior significa:
- tomar cada línea de las redes combinadas
alist
yblist
y hacer una tupla de ellas(aline, bline)
- Ahora combine estas listas con
izip
nuevamente y tome cada elemento de ellas (pair
).
Este método tiene dos ventajas:
- no hay índices usados en ningún lado
- no tiene que crear listas con
zip
y usar generadores más eficientes conizip
lugar.
for d1 in alist
for d2 in d1
if d2 = "whatever"
do_my_thing()
Si las dos listas 2D permanecen constantes durante la vida útil de su juego y no puede disfrutar de la herencia múltiple de Python para unirse a las clases de objetos alist [i] [j] y blist [i] [j] (como otros han sugerido), podría agregar un puntero al elemento b correspondiente en cada elemento después de crear las listas, como este:
for a_row, b_row in itertools.izip(alist, blist):
for a_item, b_item in itertools.izip(a_row, b_row):
a_item.b_item= b_item
Aquí se pueden aplicar varias optimizaciones, como las clases que tienen __slots__
definido, o el código de inicialización anterior podría fusionarse con su propio código de inicialización, etc. Después de eso, su ciclo se convertirá en:
for a_row in alist:
for a_item in a_row:
if a_item.isWhatever():
a_item.b_item.doSomething()
Eso debería ser más eficiente.
Si a.isWhatever
es raramente cierto podrías construir un "índice" una vez:
a_index = set((i,j)
for i,arow in enumerate(a)
for j,a in enumerate(arow)
if a.IsWhatever())
y cada vez que quieras que se haga algo:
for (i,j) in a_index:
b[i][j].doSomething()
Si cambia con el tiempo, deberá mantener el índice actualizado. Es por eso que utilicé un conjunto, por lo que los elementos se pueden agregar y eliminar rápidamente.
Puedes comprimirlos. es decir:
for a_row,b_row in zip(alist, blist):
for a_item, b_item in zip(a_row,b_row):
if a_item.isWhatever:
b_item.doSomething()
Sin embargo, la sobrecarga de comprimir e iterar sobre los elementos puede ser mayor que su método original si rara vez utiliza el elemento b (es decir, un elemento en el sitio que es generalmente falso). Podría usar itertools.izip en lugar de zip para reducir el impacto de la memoria de esto, pero probablemente aún sea un poco más lento a menos que siempre necesite el b_item.
Alternativamente, considere usar una lista 3D en su lugar, por lo que el terreno para la celda i, j está en l [i] [j] [0], objetos en l [i] [j] [1] etc., o incluso combine los objetos para que puede hacer un [i] [j] .terrain, un [i] [j] .object, etc.
[Editar] Los tiempos de DzinX en realidad muestran que el impacto del cheque adicional para b_item no es realmente significativo, junto a la penalización de rendimiento de volver a buscar por índice, por lo que lo anterior (usando izip) parece ser el más rápido.
También realicé una prueba rápida para el enfoque 3D, y parece aún más rápido, por lo que si puede almacenar sus datos en esa forma, podría ser más simple y más rápido de acceder. Aquí hay un ejemplo de usarlo:
# Initialise 3d list:
alist = [ [[A(a_args), B(b_args)] for i in xrange(WIDTH)] for j in xrange(HEIGHT)]
# Process it:
for row in xlist:
for a,b in row:
if a.isWhatever():
b.doSomething()
Aquí están mis tiempos para 10 bucles utilizando una matriz de 1000x1000, con varias proporciones de lo que sea que sea cierto son:
( Chance isWhatever is True )
Method 100% 50% 10% 1%
3d 3.422 2.151 1.067 0.824
izip 3.647 2.383 1.282 0.985
original 5.422 3.426 1.891 1.534
Si alguien está interesado en el rendimiento de las soluciones anteriores, aquí están para redes 4000x4000, del más rápido al más lento:
- Brian : 1.08s (modificado, con
izip
lugar dezip
) - John : 2.33s
- DzinX : 2.36s
- ΤΖΩΤΖΙΟΥ : 2.41s (pero la inicialización del objeto tomó 62s)
- Eugene : 3.17s
- Robert : 4.56s
- Brian : 27.24s (original, con
zip
)
EDITAR : ¡Agregó los puntajes de Brian con la modificación de izip
y ganó en gran cantidad!
La solución de John también es muy rápida, aunque usa índices (¡realmente me sorprendió ver esto!), Mientras que Robert y Brian (con zip
) son más lentos que la solución inicial del creador de la pregunta.
Entonces, presentemos la función ganadora de Brian , ya que no se muestra en forma apropiada en ningún lugar de este hilo:
from itertools import izip
for a_row,b_row in izip(alist, blist):
for a_item, b_item in izip(a_row,b_row):
if a_item.isWhatever:
b_item.doSomething()