配列
Python3
行列
多重ソート

Pythonでの行列の取り扱いの注意

リストから行列を作成する

行列を作成しようとして、面倒くさがり屋の私は次のようなコードを実行しました。

>>> raw=[0,0,0,0]
>>> matrix=[]
>>> for i in range(4):
    matrix.append(raw)

>>> matrix
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

見事に4×4の行列ができました。
これをミンコフスキー計量(意味がわからなくても問題ありません)にしようと、さらに次のような操作をしました。

matrix[0][0]=-1
>>> for i in range(3):
    matrix[i+1][i+1]=1

>>> matrix
[[-1, 1, 1, 1], [-1, 1, 1, 1], [-1, 1, 1, 1], [-1, 1, 1, 1]]

行列の1行1列に‐1を代入し、その他の対角成分には1を代入したのだから、結果は、
[[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
となるべきですが、この結果は全く意味不明です。
原因を解明すべく、行列のIDを確認してみると、

>>> id(matrix[0])
643765320
>>> id(matrix[1])
643765320
>>> id(matrix[2])
643765320
>>> id(matrix[3])
643765320
>>> 

あれ?全く同じになっています。
最初にこの現象を目撃した時は、何が起こっているのか数日混乱しました。
しかし、こういう場合はリストに値(「オブジェクト」)が代入されているのではなく、値が入っている何らかのリストのIDの参照がリンクしているだけです。つまり、この変数matrixは見かけは4×4の行列ですが、実際には要素が4個のリスト(ID:643765320)が1つだけ存在していて、そのリスト(ID:643765320)4個を縦に並べて表示しているだけです。
だから、matrix[0][0]=-1とするとすべての行の1列目(matrix[i][0])に-1が代入され、matrix[1][1]=1とするとすべての行の2列目(matrix[i][1])に1が代入され、・・・となるのです。
そして、ID:643765320であるリストの正体は(もうおわかりかと思いますが)、次のように確認できます。

>>> id(raw)
643765320

つまり、最初に作ったリストrawが実際に作成されたリストで、その後のfor文でmatrixにrawを追加し行列を作成しているように見えますが、実際にはどこを参照すればrawがあるのかというIDを与えて4×4の行列のように表示しているだけです。

このような現象を避けて多次元配列(行列を含む)を作成するための正攻法は後述しますが、私はここでcopyというモジュールにあるdeepcopy()を用いた深いコピーを知り、次にようにしてうまく作成しました。
「深いコピー」についてはこちらを参照してください→pythonでのリストの要素の変更とリストのコピー

>>> import copy
>>> raw=[0,0,0,0]
>>> matrix=[]
>>> for i in range(4):
    matrix.append(copy.deepcopy(raw))


>>> matrix[0][0]=-1
>>> for i in range(3):
    matrix[i+1][i+1]=1


>>> matrix
[[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

見事にミンコフスキー計量(この言葉の意味は知らなくても問題ありません)となりました!!

なお、よく知られていると思いますが、次のようにして行列を作成しても同様な現象が起こります。

>>> matrix=[[0]*4]*4
>>> matrix
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
>>> matrix[0][0]=-1
>>> matrix
[[-1, 0, 0, 0], [-1, 0, 0, 0], [-1, 0, 0, 0], [-1, 0, 0, 0]]
>>> id(matrix[0])
643757512
>>> id(matrix[1])
643757512

多次元配列作成の正攻法

上述のようにdeepcopy()を利用しても良いのですが、多次元配列の作成は、次のように内包表記を用いて行うのが一般的なようです。

3行4列の行列

>>> matrix = [[0 for j in range(4)] for i in range(3)]
>>> matrix
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
>>> matrix[0][0]=1
>>> matrix
[[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

入れ子構造を表しているので、列数が前で行数が後になることに注意してください。
matrix = [[0 for j in range(列数)] for i in range(行数)]

3×4×5の配列

>>> matrix = [[[0 for k in range(5)] for j in range(4)] for i in range(3)]
>>> matrix
[[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]]

少々複雑ですが、3×4×5の配列となっています。

このようにどんどん入れ子構造にしていけば、次元の高い配列が作成できます。

配列の多重ソート

少々、テーマがそれますが非常に便利な機能なので記載しておきます。
リストの要素を、数字を大きいや小さい順番にソートするコードの作成はアリゴリズムの学習テーマとしてはオーソドックですが、関数が用意されています。また、1行目について大きな順番にソートし、さらに1行目が同じである場合は2行目の要素が大きい順番にソートするなどのいわゆる「多重ソート」も関数を用いて求めることができます。

単純なリストのソート
(数字が小さな順番にソートする)
sorted()という関数を単純に用います。引数をソートしたいリストにします。

>>> list1=[1,5,8,9,10,23,4]
>>> list2=sorted(list1)
>>> list2
[1, 4, 5, 8, 9, 10, 23]

(数字が大きな順番にソートする)
引数として「reverse=True」を追記することで、降順のソートが求められます。(要素に-1を乗じた上でsorted()でソートし、さらに要素に-1を乗じて元に戻すといったことをしなくてもよい)

list1=[3,4,5,8,0,2,9,10,11]
>>> list2=sorted(list1,reverse=True)
>>> list2
[11, 10, 9, 8, 5, 4, 3, 2, 0]

なお、要素の型が文字になっている場合は数字の大きさという概念では処理されないので、注意が必要です。

list1=["1","8","6","0","34","19"]
>>> list2=sorted(list1)
>>> list2
['0', '1', '19', '34', '6', '8']

行列のソート
(行列の列を指定してソートする)
あるMatrixの3列目の数字をキーとして大きな順番にする(ベストランキング)

>>> matrix=[["kana",90,65,54],["nami",85,90,85],["kumi",75,95,60],["ayu",100,100,100],["hikaru",65,65,70]]
>>> matrix2=sorted(matrix, key=lambda x:x[2], reverse=True)
>>> matrix2
[['ayu', 100, 100, 100], ['kumi', 75, 95, 60], ['nami', 85, 90, 85], ['kana', 90, 65, 54], ['hikaru', 65, 65, 70]]

このような形式になっています。
matrix2=sorted(matrix, key=lambda x:x[キーとなる列のインデックス], reverse=True)

3列目の数字をキーとして小さな順番にする(ワーストランキング)

>>> matrix2=sorted(matrix, key=lambda x:x[2])
>>> matrix2
[['kana', 90, 65, 54], ['hikaru', 65, 65, 70], ['nami', 85, 90, 85], ['kumi', 75, 95, 60], ['ayu', 100, 100, 100]]

3列目の数字をキーとして大きな順番にし、3列目が同一の場合は2列目をキーとして大きな順番にする

>>> matrix2=sorted(matrix, key=lambda x:(x[2],x[1]), reverse=True)
>>> matrix2
[['ayu', 100, 100, 100], ['kumi', 75, 95, 60], ['nami', 85, 90, 85], ['kana', 90, 65, 54], ['hikaru', 65, 65, 70]]

matrix2=sorted(matrix, key=lambda x:(x[最初のキーとなる列のインデックス],x[*次のキーとなる列のインデックス]), reverse=True)

もう少し大きな配列でk個のキーがある場合なら、
matrix2=sorted(matrix, key=lambda x:(x[1番目のキーとなる列のインデックス],x[2番目のキーとなる列のインデックス],x[3番目のキーとなる列のインデックス],....,x[*k-1番目のキーとなる列のインデックス],x[*k番目のキーとなる列のインデックス]), reverse=True)

なお、1番目のキーについて大きな順番、2番目のキーについては小さな順番といったように降順と昇順を複合することはできないようなので、この場合 reverse=True の引数を用いるなら昇順とする列の要素に‐1を乗じて処理するか、もしくはreverseを用いないなら降順とする列の要素に-1を乗じて処理すれば良い。