Rotar una matriz bidimensional en Python

122

En un programa que estoy escribiendo, surgió la necesidad de rotar una matriz bidimensional. Buscando la solución óptima, encontré este impresionante resumen que hace el trabajo:

rotated = zip(*original[::-1])

Lo estoy usando en mi programa ahora y funciona como se supone. Sin embargo, mi problema es que no entiendo cómo funciona.

Agradecería que alguien pudiera explicar cómo las diferentes funciones involucradas logran el resultado deseado.

paldepind
fuente
7
En efecto. Lo encontré en esta pregunta SO.
paldepind

Respuestas:

96

Considere la siguiente lista bidimensional:

original = [[1, 2],
            [3, 4]]

Vamos a desglosarlo paso a paso:

>>> original[::-1]   # elements of original are reversed
[[3, 4], [1, 2]]

Esta lista se pasó a zip()utilizar el argumento de desembalar , de modo que los zipextremos de llamada a ser el equivalente a esto:

zip([3, 4],
    [1, 2])
#    ^  ^----column 2
#    |-------column 1
# returns [(3, 1), (4, 2)], which is a original rotated clockwise

Con suerte, los comentarios dejan en claro lo que ziphace, agrupará elementos de cada entrada iterable en función del índice, o en otras palabras, agrupará las columnas.

Andrew Clark
fuente
2
Uno cercano. Pero elegí el tuyo debido al pulcro arte ASCII;)
paldepind
1
y el asterisco
john ktejik
@johnktejik - esa es la parte de "desempaquetamiento de argumentos" de la respuesta, haga clic en el enlace para obtener más detalles
JR Heard
1
Para mayor claridad, debe señalar que esto gira la matriz en el sentido de las agujas del reloj y que las listas del original se convierten en tuplas.
Everett
1
para completar el círculo (obtener una lista de listas y no tuplas) Hice esto:rotated = [list(r) for r in zip(*original[::-1])]
Matt
94

Eso es un poco inteligente.

Primero, como se señaló en un comentario, en Python 3 zip()devuelve un iterador, por lo que debe encerrar todo list()para obtener una lista real, de modo que a partir de 2020 sea en realidad:

list(zip(*original[::-1]))

Aquí está el desglose:

  • [::-1]- hace una copia superficial de la lista original en orden inverso. También podría usar reversed()lo que produciría un iterador inverso sobre la lista en lugar de copiar la lista (más eficiente en memoria).
  • *- hace que cada sublista de la lista original sea un argumento separado zip()(es decir, descomprime la lista)
  • zip()- toma un elemento de cada argumento y hace una lista (bueno, una tupla) a partir de ellos, y lo repite hasta que se agotan todas las sublistas. Aquí es donde realmente ocurre la transposición.
  • list()convierte la salida de zip()en una lista.

Entonces, asumiendo que tienes esto:

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

Primero obtienes esto (copia superficial, invertida):

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

A continuación, cada una de las sublistas se pasa como argumento a zip:

zip([7, 8, 9], [4, 5, 6], [1, 2, 3])

zip() consume repetidamente un elemento desde el principio de cada uno de sus argumentos y crea una tupla a partir de él, hasta que no hay más elementos, lo que da como resultado (después de que se convierte en una lista):

[(7, 4, 1), 
 (8, 5, 2), 
 (9, 6, 3)]

Y Bob es tu tío.

Para responder a la pregunta de @ IkeMiguel en un comentario sobre rotarlo en la otra dirección, es bastante sencillo: solo necesita revertir tanto las secuencias que entran zipcomo el resultado. El primero se puede lograr quitando el [::-1]y el segundo se puede lograr lanzando un reversed()alrededor de todo. Dado que reversed()devuelve un iterador sobre la lista, tendremos que poner list()todo eso para convertirlo. Con un par de list()llamadas adicionales para convertir los iteradores en una lista real. Entonces:

rotated = list(reversed(list(zip(*original))))

Podemos simplificar eso un poco usando la porción de "emoticón marciano" en lugar de reversed()... entonces no necesitamos el exterior list():

rotated = list(zip(*original))[::-1]

Por supuesto, también puede simplemente girar la lista en el sentido de las agujas del reloj tres veces. :-)

un poco
fuente
2
¿Es posible girar en sentido antihorario?
Miguel Ike
@MiguelIke sí, hacer zip (* matriz) [:: - 1]
RYS
3
^ ¡tenga en cuenta que debe convertir el resultado de zipen una lista en Python 3.x!
RYS
17

Hay tres partes para esto:

  1. original [:: - 1] invierte la matriz original. Esta notación es la división de listas de Python. Esto le da una "sublista" de la lista original descrita por [start: end: step], start es el primer elemento, end es el último elemento que se utilizará en la sublista. step dice tomar cada elemento step'th del primero al último. El inicio y el final omitidos significa que el segmento será la lista completa, y el paso negativo significa que obtendrá los elementos al revés. Entonces, por ejemplo, si el original fuera [x, y, z], el resultado sería [z, y, x]
  2. El * cuando precede a una lista / tupla en la lista de argumentos de una llamada a función significa "expandir" la lista / tupla para que cada uno de sus elementos se convierta en un argumento separado para la función, en lugar de la lista / tupla en sí. De modo que si, por ejemplo, args = [1,2,3], zip (args) es lo mismo que zip ([1,2,3]), pero zip (* args) es lo mismo que zip (1, 2,3).
  3. zip es una función que toma n argumentos, cada uno de los cuales tiene una longitud my produce una lista de longitud m, los elementos de son de longitud n y contienen los elementos correspondientes de cada una de las listas originales. Por ejemplo, zip ([1,2], [a, b], [x, y]) es [[1, a, x], [2, b, y]]. Consulte también la documentación de Python.
setrofim
fuente
+1 ya que tú eras el único que probablemente explicaba el primer paso.
paldepind
8

Solo una observación. La entrada es una lista de listas, pero la salida de la muy buena solución: rotated = zip (* original [:: - 1]) devuelve una lista de tuplas.

Esto puede ser un problema o no.

Sin embargo, se corrige fácilmente:

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


def rotated(array_2d):
    list_of_tuples = zip(*array_2d[::-1])
    return [list(elem) for elem in list_of_tuples]
    # return map(list, list_of_tuples)

print(list(rotated(original)))

# [[7, 4, 1], [8, 5, 2], [9, 6, 3]]

La composición de la lista o el mapa convertirán las tuplas interiores de nuevo en listas.

Marcas
fuente
2
def ruota_orario(matrix):
   ruota=list(zip(*reversed(matrix)))
   return[list(elemento) for elemento in ruota]
def ruota_antiorario(matrix):
   ruota=list(zip(*reversed(matrix)))
   return[list(elemento)[::-1] for elemento in ruota][::-1]
usuario9402118
fuente
4
Explique su solución para que otros puedan entenderla mejor.
HelloSpeakman
por supuesto, la primera función (ruota_antiorario) gira en sentido antihorario y la segunda función (ruota_orario) en sentido horario
user9402118
1

Yo mismo tuve este problema y encontré la gran página de wikipedia sobre el tema (en el párrafo "Rotaciones comunes":
https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities

Luego escribí el siguiente código, muy detallado para tener una comprensión clara de lo que está sucediendo.

Espero que le resulte útil profundizar más en la hermosa e inteligente frase que ha publicado.

Para probarlo rápidamente, puede copiarlo / pegarlo aquí:
http://www.codeskulptor.org/

triangle = [[0,0],[5,0],[5,2]]
coordinates_a = triangle[0]
coordinates_b = triangle[1]
coordinates_c = triangle[2]

def rotate90ccw(coordinates):
    print "Start coordinates:"
    print coordinates
    old_x = coordinates[0]
    old_y = coordinates[1]
# Here we apply the matrix coming from Wikipedia
# for 90 ccw it looks like:
# 0,-1
# 1,0
# What does this mean?
#
# Basically this is how the calculation of the new_x and new_y is happening:
# new_x = (0)(old_x)+(-1)(old_y)
# new_y = (1)(old_x)+(0)(old_y)
#
# If you check the lonely numbers between parenthesis the Wikipedia matrix's numbers
# finally start making sense.
# All the rest is standard formula, the same behaviour will apply to other rotations, just
# remember to use the other rotation matrix values available on Wiki for 180ccw and 170ccw
    new_x = -old_y
    new_y = old_x
    print "End coordinates:"
    print [new_x, new_y]

def rotate180ccw(coordinates):
    print "Start coordinates:"
    print coordinates
    old_x = coordinates[0]
    old_y = coordinates[1] 
    new_x = -old_x
    new_y = -old_y
    print "End coordinates:"
    print [new_x, new_y]

def rotate270ccw(coordinates):
    print "Start coordinates:"
    print coordinates
    old_x = coordinates[0]
    old_y = coordinates[1]  
    new_x = -old_x
    new_y = -old_y
    print "End coordinates:"
    print [new_x, new_y]

print "Let's rotate point A 90 degrees ccw:"
rotate90ccw(coordinates_a)
print "Let's rotate point B 90 degrees ccw:"
rotate90ccw(coordinates_b)
print "Let's rotate point C 90 degrees ccw:"
rotate90ccw(coordinates_c)
print "=== === === === === === === === === "
print "Let's rotate point A 180 degrees ccw:"
rotate180ccw(coordinates_a)
print "Let's rotate point B 180 degrees ccw:"
rotate180ccw(coordinates_b)
print "Let's rotate point C 180 degrees ccw:"
rotate180ccw(coordinates_c)
print "=== === === === === === === === === "
print "Let's rotate point A 270 degrees ccw:"
rotate270ccw(coordinates_a)
print "Let's rotate point B 270 degrees ccw:"
rotate270ccw(coordinates_b)
print "Let's rotate point C 270 degrees ccw:"
rotate270ccw(coordinates_c)
print "=== === === === === === === === === "
Pitto
fuente
-1

Rotación en sentido antihorario (pivote estándar de columna a fila) como lista y dictado

rows = [
  ['A', 'B', 'C', 'D'],
  [1,2,3,4],
  [1,2,3],
  [1,2],
  [1],
]

pivot = []

for row in rows:
  for column, cell in enumerate(row):
    if len(pivot) == column: pivot.append([])
    pivot[column].append(cell)

print(rows)
print(pivot)
print(dict([(row[0], row[1:]) for row in pivot]))

Produce:

[['A', 'B', 'C', 'D'], [1, 2, 3, 4], [1, 2, 3], [1, 2], [1]]
[['A', 1, 1, 1, 1], ['B', 2, 2, 2], ['C', 3, 3], ['D', 4]]
{'A': [1, 1, 1, 1], 'B': [2, 2, 2], 'C': [3, 3], 'D': [4]}
Paul Kenjora
fuente
1
Esto no está relacionado con la pregunta, que pide una explicación de cómo zip(*original[::-1])funciona.
kaya3