2008-10-09 9 views
22

Si estoy haciendo un juego simple basado en la cuadrícula, 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.¿Cómo puedo, en Python, iterar sobre múltiples listas 2d a la vez, limpiamente?

for i in range(len(alist)): 
    for j in range(len(alist[i])): 
     if alist[i][j].isWhatever: 
      blist[i][j].doSomething() 

¿Existe alguna manera más agradable de hacer algo como esto?

Respuesta

15

me gustaría empezar 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]) 

Entonces cada vez que necesita para iterar sobre las listas de su código es el siguiente:

for (a, b) in grid_objects(alist, blist): 
    if a.is_whatever(): 
     b.do_something() 
+0

Esto no es lo mismo, en el segundo para el rango tomó el len de alist [i], ¿por qué eliminaste ese índice? –

+0

Definitivamente me gusta este. Puede que no sea la mejor respuesta, pero es bastante probable que lo use. –

+0

Esto es solo azúcar de sintaxis sobre lo que escribió Eugene. Me temo que este generador podría ser muy lento si las redes son lo suficientemente grandes. Después de todo, cada rendimiento aún requiere cuatro búsquedas de índice. – DzinX

-4
for d1 in alist 
    for d2 in d1 
     if d2 = "whatever" 
      do_my_thing() 
+0

te perdiste la lista –

10

Puede 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 compresión y iterar sobre los elementos puede ser mayor que su método original si rara vez utiliza realmente el b_item (es decir a_item.isWhatever suele ser falso). Podría usar itertools.izip en lugar de zip para reducir el impacto de la memoria de esto, pero aún así probablemente será un poco más lento a menos que siempre necesite el b_item.

Alternativamente, considere usar una lista 3D en su lugar, para que el terreno para la celda i, j esté en l [i] [j] [0], objetos en l [i] [j] [1] etc., o incluso combinar los objetos para que pueda hacer un [i] [j] .terrain, un [i] [j] .objeto, etc.

[Editar] DzinX's timings en realidad muestran que el impacto del cheque adicional para b_item no es realmente significativo, junto a la penalización de rendimiento al volver a buscar por índice, por lo que lo anterior (usando izip) parece ser el más rápido.

También he dado una prueba rápida para el enfoque 3D, y parece más rápido aún, así que si puede almacenar sus datos en esa forma, podría ser más simple y más rápido de acceder. He aquí un ejemplo de su uso:

# 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 de 10 bucles utilizando una matriz de 1000x1000, con diferentes proporciones de isWhatever ser fiel 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 
+1

Esta es la solución más rápida aquí, pero solo si cambias zip a itertools.izip (mira mi publicación en algún lugar más abajo). – DzinX

2

¿Estás seguro de que los objetos de la 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 ambos isWhatever() y doSomething()?

3

Generator expressions y izip de itertools module hará 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() 

El línea en for declaración anterior significa:

  • tomar cada línea de rejillas combinados alist y blist y crea una tupla de ellos (aline, bline)
  • ahora combinar estas listas con izip de nuevo y tomar cada elemento de ellos (pair).

Este método tiene dos ventajas:

  • no hay índices utilizados en cualquier lugar
  • usted no tiene que crear listas con zip y el uso de generadores más eficientes con izip lugar.
+0

¿Es esto más agradable? ¿Es esto más legible? –

+0

Creo que sí, sí. Al eliminar índices, se vuelve más explícito en lo que está tratando de lograr aquí (manipular objetos, no sus posiciones). – DzinX

+0

Este es claramente el mejor enfoque. –

3

Como un ligero cambio de estilo, se puede usar de enumeración:

for i, arow in enumerate(alist): 
    for j, aval in enumerate(arow): 
     if aval.isWhatever(): 
      blist[i][j].doSomething() 

No creo que obtendrá algo mucho más simple a menos que reorganizar sus estructuras de datos como sugiere Federico. Para que puedas convertir la última línea en algo así como "aval.b.doSomething()".

1

Si los dos 2D-listas se mantienen constantes durante la vida de su juego y no se puede disfrutar de la herencia múltiple de Python para unirse a la alista [i] [j] y blist [i] [j] clases de objetos (como han sugerido otros), se puede añadir un puntero al elemento b correspondiente en cada un elemento una vez creadas las listas, así:

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 

Varias optimizaciones pueden aplicar aquí, al igual que las clases que tienen __slots__ definido, o el código de inicialización anterior podría fusionarse con su propia inicialización código de ion e.t.c. 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.

+0

Esa es definitivamente una respuesta inesperada. Bastante interesante. –

32

Si alguien está interesado en el rendimiento de las soluciones anteriores, aquí están de 4000x4000 rejillas, del más rápido al más lento:

EDITAR: Los resultados de Agregado Brian con izip modificación y ganó por una 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.

Así que vamos a presentar Brian 's función de ganadores, ya que no se muestra en forma adecuada en cualquier lugar en 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() 
+0

¿Has probado la solución de Brian tal cual o con itertools.izip? Además, ¿te gustaría comparar mi sugerencia también? – tzot

+0

Agregado, y los resultados son nuevamente sorprendentes. En su caso, parece que agregar una variable inesperada (no en __slots__) a un objeto es muy lenta. – DzinX

+0

Oh, la inicialización se puede combinar con la inicialización de matriz existente. No sé por qué mencionas la parte "no en __slots__"; no sabemos cómo se implementan los objetos de celda. Por cierto, olvidaste llamar a_item.isWhatever (es decir, no hay paréntesis) – tzot

4

Cuando se opera con rejillas de números y desea realmente un buen rendimiento, se debe considerar usando 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 con 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 
0

Si a.isWhatever rara vez es cierto que podría construir un "índice" de 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 quiere que se haga algo:

for (i,j) in a_index: 
    b[i][j].doSomething() 

Si a cambios en el tiempo , entonces necesitará mantener el índice actualizado. Es por eso que usé un conjunto, por lo que los elementos se pueden agregar y eliminar rápidamente.

Cuestiones relacionadas