Rotação de uma matriz bidimensional em Python

122

Em um programa que estou escrevendo, surgiu a necessidade de girar um array bidimensional. Procurando a solução ideal, encontrei este impressionante one-liner que faz o trabalho:

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

Estou usando no meu programa agora e funciona como deveria. Meu problema, porém, é que não entendo como funciona.

Eu apreciaria se alguém pudesse explicar como as diferentes funções envolvidas alcançam o resultado desejado.

paldepind
fonte
7
De fato. Eu encontrei nesta pergunta SO.
paldepind

Respostas:

96

Considere a seguinte lista bidimensional:

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

Vamos dividir passo a passo:

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

Essa lista é passada para o zip()uso de desempacotamento de argumento , então a zipchamada acaba sendo o equivalente a isto:

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

Esperançosamente, os comentários deixam claro o que zipsignifica, ele agrupará os elementos de cada entrada iterável com base no índice ou, em outras palavras, agrupará as colunas.

Andrew Clark
fonte
2
Um próximo. Mas eu escolhi o seu devido à arte ASCII
bacana
1
e o asterisco ??
john ktejik
@johnktejik - essa é a parte "desempacotamento do argumento" da resposta, clique no link para obter detalhes
JR Heard de
1
Para maior clareza, você deve apontar que isso gira a matriz no sentido horário e que as listas do original são convertidas em tuplas.
Everett
1
para completar o círculo (obter uma lista de listas e não de tuplas) Eu fiz isso:rotated = [list(r) for r in zip(*original[::-1])]
matt
94

Essa é uma parte inteligente.

Primeiro, conforme observado em um comentário, em Python 3 zip()retorna um iterador, então você precisa incluir tudo list()para obter uma lista real de volta, então a partir de 2020 é realmente:

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

Aqui está o detalhamento:

  • [::-1]- faz uma cópia superficial da lista original na ordem inversa. Também poderia usar o reversed()que produziria um iterador reverso sobre a lista em vez de realmente copiar a lista (mais eficiente em termos de memória).
  • *- torna cada sublista na lista original um argumento separado para zip()(ou seja, descompacta a lista)
  • zip()- pega um item de cada argumento e faz uma lista (bem, uma tupla) a partir deles, e repete até que todas as sublistas sejam esgotadas. É aqui que a transposição realmente acontece.
  • list()converte a saída de zip()em uma lista.

Então, supondo que você tenha isso:

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

Primeiro você obtém isto (cópia superficial e invertida):

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

Em seguida, cada uma das sublistas é passada como um argumento para zip:

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

zip() consome repetidamente um item do início de cada um de seus argumentos e faz uma tupla a partir dele, até que não haja mais itens, resultando em (após ser convertido em uma lista):

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

E Bob é seu tio.

Para responder à pergunta de @IkeMiguel em um comentário sobre girá-lo na outra direção, é bastante simples: você só precisa inverter as sequências que entram zipe o resultado. O primeiro pode ser alcançado removendo o [::-1]e o segundo pode ser alcançado jogando um em reversed()volta de tudo. Uma vez que reversed()retorna um iterador sobre a lista, precisaremos list()contorná- lo para convertê-lo. Com algumas list()chamadas extras para converter os iteradores em uma lista real. Assim:

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

Podemos simplificar isso um pouco usando a fatia "smiley marciano" em vez de reversed()... então não precisamos do exterior list():

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

Claro, você também pode simplesmente girar a lista no sentido horário três vezes. :-)

kindall
fonte
2
É possível girar no sentido anti-horário ??
Miguel Ike,
@MiguelIke sim, faça zip (* matriz) [:: - 1]
RYS
3
^ note que você deve converter o resultado de zippara uma lista no Python 3.x!
RYS
17

Existem três partes para isso:

  1. original [:: - 1] inverte a matriz original. Esta notação é o fatiamento da lista Python. Isso fornece uma "sublista" da lista original descrita por [início: fim: etapa], início é o primeiro elemento, final é o último elemento a ser usado na sublista. etapa diz: pegue cada elemento da etapa, do primeiro ao último. O início e o fim omitidos significam que a fatia será a lista inteira e a etapa negativa significa que você obterá os elementos ao contrário. Então, por exemplo, se o original fosse [x, y, z], o resultado seria [z, y, x]
  2. O * quando precede uma lista / tupla na lista de argumentos de uma chamada de função significa "expandir" a lista / tupla de modo que cada um de seus elementos se torne um argumento separado para a função, ao invés da própria lista / tupla. De modo que se, digamos, args = [1,2,3], então zip (args) é o mesmo que zip ([1,2,3]), mas zip (* args) é o mesmo que zip (1, 2,3).
  3. zip é uma função que recebe n argumentos, cada um dos quais tem comprimento me produz uma lista de comprimento m, os elementos de são de comprimento n e contêm os elementos correspondentes de cada uma das listas originais. Por exemplo, zip ([1,2], [a, b], [x, y]) é [[1, a, x], [2, b, y]]. Veja também a documentação do Python.
setrofim
fonte
+1 já que você foi o único provavelmente a explicar o primeiro passo.
paldepind
8

Apenas uma observação. A entrada é uma lista de listas, mas a saída da solução muito boa: rotated = zip (* original [:: - 1]) retorna uma lista de tuplas.

Isso pode ou não ser um problema.

No entanto, é facilmente corrigido:

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]]

A composição de lista ou o mapa converterão as tuplas internas de volta em listas.

MarkS
fonte
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]
user9402118
fonte
4
Explique sua solução para que outras pessoas possam entendê-la melhor.
HelloSpeakman
é claro, a primeira função (ruota_antiorario) gira no sentido anti-horário e a segunda função (ruota_orario) no sentido horário
user9402118
1

Eu mesmo tive esse problema e encontrei a ótima página da wikipedia sobre o assunto (no parágrafo "Rotações comuns":
https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities

Então escrevi o seguinte código, super prolixo, para ter uma compreensão clara do que está acontecendo.

Espero que você ache útil cavar mais no muito bonito e inteligente one-liner que você postou.

Para testá-lo rapidamente, você pode copiar / colar aqui:
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
fonte
-1

Girando no sentido anti-horário (coluna padrão para pivô de linha) como lista e ditado

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]))

Produz:

[['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
fonte
1
Isso não está relacionado à pergunta, que pede uma explicação de como zip(*original[::-1]) funciona.
kaya3,