Rotating a two-dimensional array in Python
In a program I'm writing the need to rotate a two-dimensional array came up. Searching for the optimal solution I found this impressive one-liner that does the job:
rotated = zip(*original[::-1])
I'm using it in my program now and it works as supposed. My problem though, is that I don't understand how it works.
I'd appreciate if someone could explain how the different functions involved achieves the desired result.
That's a clever bit.
First, as noted in a comment, in Python 3 zip()
returns an iterator, so you need to enclose the whole thing in list()
to get an actual list back out, so as of 2020 it's actually:
list(zip(*original[::-1]))
Here's the breakdown:
-
[::-1]
- makes a shallow copy of the original list in reverse order. Could also usereversed()
which would produce a reverse iterator over the list rather than actually copying the list (more memory efficient). -
*
- makes each sublist in the original list a separate argument tozip()
(i.e., unpacks the list) -
zip()
- takes one item from each argument and makes a list (well, a tuple) from those, and repeats until all the sublists are exhausted. This is where the transposition actually happens. -
list()
converts the output ofzip()
to a list.
So assuming you have this:
[ [1, 2, 3],
[4, 5, 6],
[7, 8, 9] ]
You first get this (shallow, reversed copy):
[ [7, 8, 9],
[4, 5, 6],
[1, 2, 3] ]
Next each of the sublists is passed as an argument to zip
:
zip([7, 8, 9], [4, 5, 6], [1, 2, 3])
zip()
repeatedly consumes one item from the beginning of each of its arguments and makes a tuple from it, until there are no more items, resulting in (after it's converted to a list):
[(7, 4, 1),
(8, 5, 2),
(9, 6, 3)]
And Bob's your uncle.
To answer @IkeMiguel's question in a comment about rotating it in the other direction, it's pretty straightforward: you just need to reverse both the sequences that go into zip
and the result. The first can be achieved by removing the [::-1]
and the second can be achieved by throwing a reversed()
around the whole thing. Since reversed()
returns an iterator over the list, we will need to put list()
around that to convert it. With a couple extra list()
calls to convert the iterators to an actual list. So:
rotated = list(reversed(list(zip(*original))))
We can simplify that a bit by using the "Martian smiley" slice rather than reversed()
... then we don't need the outer list()
:
rotated = list(zip(*original))[::-1]
Of course, you could also simply rotate the list clockwise three times. :-)
Consider the following two-dimensional list:
original = [[1, 2],
[3, 4]]
Lets break it down step by step:
>>> original[::-1] # elements of original are reversed
[[3, 4], [1, 2]]
This list is passed into zip()
using argument unpacking, so the zip
call ends up being the equivalent of this:
zip([3, 4],
[1, 2])
# ^ ^----column 2
# |-------column 1
# returns [(3, 1), (4, 2)], which is a original rotated clockwise
Hopefully the comments make it clear what zip
does, it will group elements from each input iterable based on index, or in other words it groups the columns.
There are three parts to this:
- original[::-1] reverses the original array. This notation is Python list slicing. This gives you a "sublist" of the original list described by [start:end:step], start is the first element, end is the last element to be used in the sublist. step says take every step'th element from first to last. Omitted start and end means the slice will be the entire list, and the negative step means that you'll get the elements in reverse. So, for example, if original was [x,y,z], the result would be [z,y,x]
- The * when preceding a list/tuple in the argument list of a function call means "expand" the list/tuple so that each of its elements becomes a separate argument to the function, rather than the list/tuple itself. So that if, say, args = [1,2,3], then zip(args) is the same as zip([1,2,3]), but zip(*args) is the same as zip(1,2,3).
- zip is a function that takes n arguments each of which is of length m and produces a list of length m, the elements of are of length n and contain the corresponding elements of each of the original lists. E.g., zip([1,2],[a,b],[x,y]) is [[1,a,x],[2,b,y]]. See also Python documentation.