ライブラリ無しで配列を回転させたい!

趣旨

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. (1, 4, 7)

  2. (2, 5, 8)

  3. (3, 6, 9)

の順番でitemsに代入されます。

rotated_matrix.append(list(reversed(items)))

itemsをひっくり返した物をリストとしてrotated_matrixに追加してます。
つまり、

  1. (1, 4, 7) → [7, 4, 1]

  2. (2, 5, 8) → [8, 5, 2]

  3. (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. [1, 2, 3] → [3, 2, 1]

  2. [4, 5, 6] → [6, 5, 4]

  3. [7, 8, 9] → [9, 8, 7]

つまり、

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

matrixの中身はこのように変換されます。

for items in zip(*matrix):

もうお馴染みzip関数で要素をitemsに代入。

  1. (3, 6, 9)

  2. (2, 5, 8)

  3. (1, 4, 7)

これを、list型にキャストしてrotated_matrixに追加する。

rotated_matrix.append(list(items))

左回転完了!


最後に…

たぶんこのクソコードをそのまま使う人はいないでしょう。
ですが、このコードを通して関数の使い方の理解につながれば幸いです。

この記事が気に入ったらサポートをしてみませんか?