Obracanie dwuwymiarowej tablicy w Pythonie

122

W programie, który piszę, pojawiła się potrzeba obracania dwuwymiarowej tablicy. Szukając optymalnego rozwiązania znalazłem ten imponujący jednolinijkowy, który spełnia swoje zadanie:

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

Używam go teraz w moim programie i działa zgodnie z oczekiwaniami. Mój problem polega jednak na tym, że nie rozumiem, jak to działa.

Byłbym wdzięczny, gdyby ktoś mógł wyjaśnić, w jaki sposób różne zaangażowane funkcje osiągają pożądany rezultat.

paldepind
źródło
7
W rzeczy samej. Znalazłem to w tym pytaniu SO.
paldepind

Odpowiedzi:

96

Rozważ następującą dwuwymiarową listę:

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

Podzielmy to krok po kroku:

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

Ta lista jest przekazywana zip()przy użyciu funkcji rozpakowywania argumentów , więc zipwywołanie kończy się tym, że jest odpowiednikiem tego:

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

Miejmy nadzieję, że komentarze wyjaśniają, co ziprobi, zgrupuje elementy z każdego wejścia iterowalnego na podstawie indeksu, lub innymi słowy grupuje kolumny.

Andrew Clark
źródło
2
Bliski. Ale wybrałem twoją ze względu na schludną
grafikę
1
i gwiazdka ??
john ktejik
@johnktejik - to część odpowiedzi dotycząca „rozpakowywania argumentów”, kliknij link, aby uzyskać szczegółowe informacje
JR Heard
1
Dla jasności należy zauważyć, że powoduje to obrót macierzy zgodnie z ruchem wskazówek zegara, a listy z oryginału są konwertowane na krotki.
Everett
1
aby przejść do pełnego koła (odzyskać listę list, a nie krotek) Zrobiłem to:rotated = [list(r) for r in zip(*original[::-1])]
mat.
94

To sprytne rozwiązanie.

Po pierwsze, jak zauważono w komentarzu, w Pythonie 3 zip()zwraca iterator, więc musisz załączyć całość, list()aby uzyskać rzeczywistą listę z powrotem, więc od 2020 r.

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

Oto podział:

  • [::-1]- sporządza płytką kopię oryginalnej listy w odwrotnej kolejności. Można również użyć, reversed()który utworzyłby odwrotny iterator po liście zamiast kopiowania listy (bardziej wydajna pamięć).
  • *- sprawia, że ​​każda podlista w oryginalnej liście jest oddzielnym argumentem zip()(tzn. rozpakowuje listę)
  • zip()- pobiera jedną pozycję z każdego argumentu i tworzy z nich listę (cóż, krotkę) i powtarza, aż wszystkie podlisty zostaną wyczerpane. W tym miejscu faktycznie zachodzi transpozycja.
  • list()konwertuje wynik programu zip()na listę.

Zakładając, że masz to:

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

Najpierw dostajesz to (płytka, odwrócona kopia):

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

Następnie każda z podlist jest przekazywana jako argument do zip:

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

zip() wielokrotnie zużywa jeden element od początku każdego ze swoich argumentów i tworzy z niego krotkę, aż nie będzie już żadnych elementów, w wyniku czego (po przekonwertowaniu na listę):

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

Bob jest twoim wujem.

Aby odpowiedzieć na pytanie @ IkeMiguel w komentarzu na temat obracania go w innym kierunku, jest to całkiem proste: wystarczy odwrócić zarówno sekwencje, które wchodzą, jak zipi wynik. Pierwszą można osiągnąć, usuwając, [::-1]a drugą, rzucając reversed()wokół całości. Ponieważ reversed()zwraca iterator na liście, będziemy musieli go list()obejść , aby ją przekonwertować. Z kilkoma dodatkowymi list()wywołaniami, aby przekonwertować iteratory na rzeczywistą listę. Więc:

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

Możemy to nieco uprościć, używając plastra „marsjańskiej buźki” zamiast reversed()… wtedy nie potrzebujemy zewnętrznego list():

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

Oczywiście możesz też po prostu trzykrotnie obrócić listę w prawo. :-)

kindall
źródło
2
Czy można obracać w lewo?
Miguel Ike
@MiguelIke yes, do zip (* matrix) [:: - 1]
RYS
3
^ zwróć uwagę, że zipw Pythonie 3.x musisz rzutować wynik na listę!
RYS,
17

Są na to trzy części:

  1. original [:: - 1] odwraca oryginalną tablicę. Ta notacja to wycinanie list w Pythonie. W ten sposób otrzymasz „podlistę” pierwotnej listy opisanej przez [start: end: step], początek to pierwszy element, koniec to ostatni element, który zostanie użyty w podliście. krok mówi, że każdy krok jest krokiem od pierwszego do ostatniego. Pominięty początek i koniec oznacza, że ​​wycinek będzie całą listą, a krok ujemny oznacza, że ​​otrzymasz elementy w odwrotnej kolejności. Na przykład, jeśli oryginał to [x, y, z], wynikiem będzie [z, y, x]
  2. Znak * poprzedzający listę / krotkę w liście argumentów wywołania funkcji oznacza „rozwinięcie” listy / krotki, tak aby każdy z jej elementów stał się oddzielnym argumentem funkcji, a nie samą listą / krotką. Więc jeśli, powiedzmy, args = [1,2,3], to zip (args) to to samo co zip ([1,2,3]), ale zip (* args) to to samo co zip (1, 2,3).
  3. zip to funkcja, która przyjmuje n argumentów, z których każdy ma długość m i tworzy listę o długości m, elementy mają długość n i zawierają odpowiednie elementy każdej z oryginalnych list. Np. Zip ([1,2], [a, b], [x, y]) to [[1, a, x], [2, b, y]]. Zobacz także dokumentację Pythona.
setrofim
źródło
+1, ponieważ ty byłeś jedynym prawdopodobnie wyjaśniającym pierwszy krok.
paldepind
8

Tylko obserwacja. Wejście to lista list, ale wynik bardzo fajnego rozwiązania: rotated = zip (* original [:: - 1]) zwraca listę krotek.

Może to stanowić problem, ale nie musi.

Można to jednak łatwo poprawić:

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

Kompozycja list lub mapa przekonwertują wewnętrzne krotki z powrotem na listy.

Znaki
źródło
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
źródło
4
Proszę wyjaśnij swoje rozwiązanie, aby inni mogli je lepiej zrozumieć.
HelloSpeakman
oczywiście pierwsza funkcja (ruota_antiorario) obraca się w kierunku przeciwnym do ruchu wskazówek zegara, a druga funkcja (ruota_orario) zgodnie z ruchem wskazówek zegara
user9402118
1

Sam miałem ten problem i znalazłem świetną stronę Wikipedii na ten temat (w akapicie „Wspólne rotacje”:
https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities

Następnie napisałem następujący kod, super gadatliwy, aby dobrze zrozumieć, co się dzieje.

Mam nadzieję, że okażą się przydatne, aby zagłębić się w tę bardzo piękną i sprytną linijkę, którą opublikowałeś.

Aby szybko to przetestować, możesz skopiować / wkleić tutaj:
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
źródło
-1

Obracanie przeciwnie do ruchu wskazówek zegara (standardowa kolumna do wiersza) As List i Dict

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

Produkuje:

[['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
źródło
1
Nie ma to związku z pytaniem, które prosi o wyjaśnienie, jak zip(*original[::-1])działa.
kaya3