ライブラリ無しで配列を回転させたい!
趣旨
Pythonで二次元配列を回転させる時は、普通ならNumpyとかで回すはず。
とはいえ、
いろんな事情でライブラリをインストールできない!
でも二次元配列を回転させなければならない!
なんて状況もありますよね?
私は昔一度だけありました。(レアケースじゃねーか)
お客さんのルールとか環境とか、諸事情でどうしてもダメ!
みたいなこともゼロじゃないんだよなぁ・・・。
そんなこんなで、今回は純粋なPythonだけで2次元配列を回転させる。
なるべく使いやすいようにまとめた状態でGitHubで公開してるので、
好きに使ってください。
クソコードなのは許していただきたく。
目次
環境
とりあえずPythonが動けばなんでも大丈夫です。
Python3.6以降(GitHubのコードは型アノテーションがついてるので)
とりあえず回そうぜ
ごちゃごちゃ言う前に、とりあえず回してみましょう。
入力する二次元配列は以下の通りに定義します。
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
右回転(時計回り)
右に90度回転させるコードは以下の通り。
まずは処理の流れがイメージしやすいように、通常のfor文で書きます。
rotated_matrix = []
for items in zip(*matrix):
rotated_matrix.append(list(reversed(items)))
print(*rotated_matrix, sep='\n')
なるべく高速に処理するために、内包表記でも書きましょう。
matrix = [list(reversed(items)) for items in zip(*matrix)]
print(*matrix, sep='\n')
以下のような出力が得られるはず。
[7, 4, 1]
[8, 5, 2]
[9, 6, 3]
右に90度回転していることが確認できましたね。
左回転(反時計回り)
今度は左に90度回転させるコード。
rotated_matrix = []
matrix = map(reversed, matrix)
for items in zip(*matrix):
rotated_matrix.append(list(items))
print(*rotated_matrix, sep='\n')
こちらも同様に内包表記バージョンも書きましょう。
matrix = [list(items) for items in zip(*map(reversed, matrix))]
print(*matrix, sep='\n')
出力は以下の通り。
[3, 6, 9]
[2, 5, 8]
[1, 4, 7]
左に90度回転ましたね。
解説
コードの中の表現の解釈に困る人用の語弊たっぷり解説をします。
鵜呑みは厳禁。
ただ、動きとしては正しいはず。
おそらくこの表現で困るかな?
コード中に出てくる表現の中で、以下の物で困ると思います。
*matrix … アンパック演算子
zip() … zip関数
list() … list関数
map() … map関数
reversed() … reversed関数
上から順番に解説します。
*matrix … アンパック演算子
配列の要素を個別に取り出すことができる書き方。
以下のコードでイメージを掴みましょう。
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
# アンパック演算子での表現
print(*matrix)
# アンパック演算子を使わない表現
print(matrix[0], matrix[1], matrix[2])
# 出力結果だけ一致させた表現
print([1, 2, 3], [4, 5, 6], [7, 8, 9])
上記のprint文の出力結果は全て同じです。
つまり、アンパック演算子は、
配列の要素を全部配列から取り出して並べてくれるイメージ。
zip() … zip関数
複数のリストを同時に頭から取り出してくれる関数。
list_a = [1, 2, 3]
list_b = [4, 5, 6]
list_c = [7, 8, 9]
for item1, item2, item3 in zip(list_a, list_b, list_c):
print(item1, item2, item3)
上記のコードを実行すると、以下の出力が得られるはず。
1 4 7
2 5 8
3 6 9
お分かりいただけただろうか?
zip関数に入れたlist_a, list_b, list_cの要素が頭から順番にitem1, item2, item3に格納されていることが確認できました。
つまり、以下のコードと挙動は同じです。
list_a = [1, 2, 3]
list_b = [4, 5, 6]
list_c = [7, 8, 9]
for i in range(min(len(list_a), len(list_b), len(list_c))):
print(list_a[i], list_b[i], list_c[i])
range関数の値の指定が少しややこしく見えるかもしれないが、
この書き方でzip関数の動きが再現できるようになってます。
len関数は配列の要素数(上記だと全て3)で、
min関数は与えた数字の中から最小の物を返してくれます。
つまり、
range(配列の要素数の中で最も要素が少ない物)
と指定していることになる。
これがまさにzip関数の動き。
例えば、
list_a = [1, 2, 3, 1]
list_b = [4, 5, 6, 2, 2, 2]
list_c = [7, 8, 9, 3, 3]
for item1, item2, item3 in zip(list_a, list_b, list_c):
print(item1, item2, item3)
このように、zip関数に要素数がバラバラのリストを渡した場合、
どうなるでしょうか?
答えは以下の通りです。
1 4 7
2 5 8
3 6 9
1 2 3
見ての通り、zip関数に渡したリストの中で最も要素数の少ない物を基準に
ループ処理を実行することになります。
list() … list関数
リスト型に型変換してくれる関数。
list_a = [1, 2, 3, 1]
list_b = [4, 5, 6, 2, 2, 2]
list_c = [7, 8, 9, 3, 3]
for item in zip(list_a, list_b, list_c):
print(item)
上記のように、zip関数の戻り値をitemだけで受け取った場合、
itemはタプル型で返ってきます。
以下のような感じ。
(1, 4, 7)
(2, 5, 8)
(3, 6, 9)
(1, 2, 3)
この戻ってきたタプルをリスト型に変換するのがリスト関数。
list_a = [1, 2, 3, 1]
list_b = [4, 5, 6, 2, 2, 2]
list_c = [7, 8, 9, 3, 3]
for item in zip(list_a, list_b, list_c):
print(list(item))
出力は以下の通り。
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]
[1, 2, 3]
同じように、リストをタプルやセットにしたい時も、
tuple()やset()で変換できます。
map() … map関数
リストの各要素に効率良く特定の処理を実行する関数。
list_string = ['1', '2', '3', '4', '5']
list_int = map(int, list_string)
for n in list_int:
print(n)
print(type(n))
このコードは、以下のものと挙動は同じ。
list_string = ['1', '2', '3', '4', '5']
for item in list_string:
n = int(item)
print(n)
print(type(n))
map(各要素に対して実行する処理, リスト)
みたいに指定すると、通常のループ処理より高効率で処理できます。
先程のコードにある、map(int, …)の「int」は、
map関数にint関数を渡していることになります。
※関数に()を付けない場合、関数の実行ではなく関数そのものを指します。
map関数に渡す処理は、自分で作った関数でも渡せます。
例えば、以下のような感じ。
def add(item):
return item + 1
def sub(item):
return item - 1
items = [1, 2, 3, 4, 5]
items_add = map(add, items)
items_sub = map(sub, items)
print(*items)
print(*items_add)
print(*items_sub)
出力は以下の通り。
1 2 3 4 5
2 3 4 5 6
0 1 2 3 4
itemsの各要素に1を足した結果と、1を引いた結果が得られました。
処理の内容は足し算、引き算以外の複雑な処理でも大丈夫。
ただし、map関数の戻り値はリストやタプルではなく、
mapオブジェクトが返ってくることに注意が必要。
mapオブジェクトはそのままfor文に入れて使えるが、
インデックスで値にアクセスすることはできません。
list関数でmapオブジェクトをリスト型にキャストすると、
インデックスで値アクセスできるようになります。
def add(item):
return item + 1
items = [1, 2, 3, 4, 5]
items_add = list(map(add, items))
for i in range(len(items_add)):
print(items_add[i])
これで問題なし。
reversed() … reversed関数
リストをひっくり返した状態で順番に取り出してくれる関数。
「順番に取り出す」が重要で、
reversed関数はreversedオブジェクトが返ります。
つまり、map関数と同じでインデックスで値にアクセスできないが、
取り扱いはmap関数と同じで問題ありません。
items = [1, 2, 3, 4, 5]
reversed_items = reversed(items)
list_reversed_items = list(reversed_items)
for i in range(len(list_reversed_items)):
print(list_reversed_items[i])
これでインデックスで値にアクセスできます。
回転の解説
右回転
上記の内容を踏まえて、右回転のコードを解説します。
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
rotated_matrix = []
for items in zip(*matrix):
rotated_matrix.append(list(reversed(items)))
print(*rotated_matrix, sep='\n')
matrixは元の二次元リスト、rotated_matrixが回転後の二次元リスト。
for items in zip(*matrix):
ここでは、matrixの各要素をzip関数に渡してます。
これで、
(1, 4, 7)
(2, 5, 8)
(3, 6, 9)
の順番でitemsに代入されます。
rotated_matrix.append(list(reversed(items)))
itemsをひっくり返した物をリストとしてrotated_matrixに追加してます。
つまり、
(1, 4, 7) → [7, 4, 1]
(2, 5, 8) → [8, 5, 2]
(3, 6, 9) → [9, 6, 3]
の順番で反転したリストがroteted_matrixに追加されます。
右回転完了!
左回転
今度は左回転。
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
rotated_matrix = []
matrix = map(reversed, matrix)
for items in zip(*matrix):
rotated_matrix.append(list(items))
print(*rotated_matrix, sep='\n')
右回転と同様にポイントだけ解説。
matrix = map(reversed, matrix)
matrixの要素を全てひっくり返します。
[1, 2, 3] → [3, 2, 1]
[4, 5, 6] → [6, 5, 4]
[7, 8, 9] → [9, 8, 7]
つまり、
matrix = [[3, 2, 1],
[6, 5, 4],
[9, 8, 7]]
matrixの中身はこのように変換されます。
for items in zip(*matrix):
もうお馴染みzip関数で要素をitemsに代入。
(3, 6, 9)
(2, 5, 8)
(1, 4, 7)
これを、list型にキャストしてrotated_matrixに追加する。
rotated_matrix.append(list(items))
左回転完了!
最後に…
たぶんこのクソコードをそのまま使う人はいないでしょう。
ですが、このコードを通して関数の使い方の理解につながれば幸いです。