Поворот двумерного массива в Python

122

В программе, которую я пишу, возникла необходимость повернуть двумерный массив. В поисках оптимального решения я нашел этот впечатляющий однострочный текст, который выполняет свою работу:

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

Сейчас я использую его в своей программе, и он работает как положено. Но моя проблема в том, что я не понимаю, как это работает.

Я был бы признателен, если бы кто-нибудь мог объяснить, как различные задействованные функции достигают желаемого результата.

paldepind
источник
7
На самом деле. Я нашел это в этом ТАК вопросе.
paldepind

Ответы:

96

Рассмотрим следующий двумерный список:

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

Давайте разберем это шаг за шагом:

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

Этот список передается zip()при использовании распаковки аргументов , поэтому zipвызов оказывается эквивалентом этого:

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

Надеюсь, комментарии прояснят, что zipделает, он группирует элементы из каждого итерируемого ввода на основе индекса, или, другими словами, он группирует столбцы.

Эндрю Кларк
источник
2
Близкий. Но я выбрал вашу из-за аккуратного ASCII-арта;)
paldepind
1
а звездочка ??
john ktejik
@johnktejik - это часть ответа, посвященная «распаковке аргументов», нажмите ссылку, чтобы узнать подробности
JR Heard
1
Для ясности вы должны указать, что это поворачивает матрицу по часовой стрелке и что списки из оригинала преобразуются в кортежи.
Эверетт
1
чтобы пройти полный круг (вернуть список списков, а не кортежей), я сделал это:rotated = [list(r) for r in zip(*original[::-1])]
matt
94

Это умно.

Во-первых, как указано в комментарии, в Python 3 zip()возвращается итератор, поэтому вам нужно заключить все это, list()чтобы получить реальный список обратно, так что с 2020 года это на самом деле:

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

Вот разбивка:

  • [::-1]- делает мелкую копию исходного списка в обратном порядке. Можно также использовать, reversed()который будет производить обратный итератор по списку, а не фактическое копирование списка (более эффективное использование памяти).
  • *- делает каждый подсписок в исходном списке отдельным аргументом для zip()(т.е. распаковывает список)
  • zip()- берет по одному элементу из каждого аргумента и составляет из них список (ну, кортеж) и повторяется, пока не будут исчерпаны все подсписки. Вот где на самом деле происходит транспозиция.
  • list()преобразует вывод zip()в список.

Итак, если у вас есть это:

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

Сначала вы получите это (неглубокая перевернутая копия):

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

Затем каждый из подсписок передается в качестве аргумента zip:

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

zip() многократно потребляет по одному элементу с начала каждого из своих аргументов и создает из него кортеж, пока не кончатся элементы, в результате чего (после преобразования в список):

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

А Боб твой дядя.

Чтобы ответить на вопрос @IkeMiguel в комментарии о его вращении в другом направлении, это довольно просто: вам просто нужно отменить как последовательность, которая входит, так zipи результат. Первое может быть достигнуто, удалив, [::-1]а второе может быть достигнуто, разбросав все reversed()вокруг. Так reversed()как по списку возвращается итератор, нам нужно будет его list()обойти , чтобы преобразовать. С парой дополнительных list()вызовов для преобразования итераторов в реальный список. Так:

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

Мы можем немного упростить это, используя срез "марсианского смайлика", а не reversed()... тогда нам не понадобится внешний list():

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

Конечно, вы также можете просто повернуть список по часовой стрелке три раза. :-)

Kindall
источник
2
Можно ли повернуть против часовой стрелки ??
Мигель Айк
@MiguelIke да, сделайте zip (* matrix) [:: - 1]
RYS
3
^ обратите внимание, что вы должны преобразовать результат zipв список в Python 3.x!
RYS
17

Это состоит из трех частей:

  1. original [:: - 1] переворачивает исходный массив. Это обозначение - нарезка списка Python. Это дает вам «подсписок» исходного списка, описанного [start: end: step], start - это первый элемент, end - последний элемент, который будет использоваться в подсписке. step говорит, что нужно выполнять каждый шаг от первого до последнего. Опущенные начало и конец означают, что срез будет целым списком, а отрицательный шаг означает, что вы получите элементы в обратном порядке. Так, например, если исходное значение было [x, y, z], результатом было бы [z, y, x]
  2. Знак * перед списком / кортежем в списке аргументов при вызове функции означает «развернуть» список / кортеж так, чтобы каждый из его элементов стал отдельным аргументом функции, а не сам список / кортеж. Так что если, скажем, args = [1,2,3], то zip (args) совпадает с zip ([1,2,3]), но zip (* args) совпадает с zip (1, 2,3).
  3. zip - это функция, которая принимает n аргументов, каждый из которых имеет длину m, и создает список длиной m, элементы имеют длину n и содержат соответствующие элементы каждого из исходных списков. Например, zip ([1,2], [a, b], [x, y]) - это [[1, a, x], [2, b, y]]. См. Также документацию Python.
setrofim
источник
+1 так как вы, вероятно, единственный, кто объясняет первый шаг.
paldepind
8

Просто наблюдение. Входные данные - это список списков, но результат очень красивого решения: rotated = zip (* original [:: - 1]) возвращает список кортежей.

Это может быть, а может и не быть проблемой.

Однако это легко исправить:

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

Композиция списка или карта преобразуют внутренние кортежи обратно в списки.

Метки
источник
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
источник
4
Пожалуйста, объясните свое решение, чтобы другие могли его лучше понять.
HelloSpeakman
конечно, первая функция (ruota_antiorario) вращается против часовой стрелки, а вторая функция (ruota_orario) по часовой стрелке
user9402118
1

У меня сама была эта проблема, и я нашел отличную страницу в Википедии по этой теме (в абзаце «Общие вращения»:
https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities

Затем я написал следующий код, очень подробный, чтобы иметь четкое представление о том, что происходит.

Я надеюсь, что вы сочтете полезным копать больше в очень красивом и умном однострочном сообщении, которое вы разместили.

Чтобы быстро протестировать его, вы можете скопировать / вставить его здесь:
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
источник
-1

Поворот против часовой стрелки (стандартный столбец для поворота строки) в виде списка и словаря

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

Производит:

[['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]}
Пол Кенджора
источник
1
Это не связано с вопросом, который требует объяснения того, как zip(*original[::-1])работает.
kaya3