Python | データ構造強化:二次元リストのループ

Python Python
スポンサーリンク

概要(二次元リストのループは「外側=行、内側=列」を丁寧にまわすのが基本)

二次元リストは「リストの中にリスト」を持つ入れ子構造です。ループは外側で行(row)を、内側で列(value)を走査するのが基本形になります。安全で読みやすいループにするために、enumerate で行・列のインデックスを得る、サイズ整合性を確認する、境界チェックを先に入れる、重い処理の前取りを行う、といったポイントを押さえましょう。転置や列抽出、条件付きの走査、集計、近傍の走査などの定番パターンを体に入れると、実務の表操作が短く堅牢に書けます。

基本の回し方(ここが重要)

外側=行、内側=列の二重ループ

二次元リストの最もシンプルな走査は、外側で行を取り、内側で列の値を取り出す形です。行番号・列番号が必要なら enumerate を使います。

M = [
    [1, 2, 3],
    [4, 5, 6],
]

for r, row in enumerate(M):
    for c, val in enumerate(row):
        print(r, c, val)
Python

行や列を一括処理したいときは、外側ループで行単位にまとめて書くと読みやすくなります。

for row in M:
    s = sum(row)     # 行の合計
    print(s)
Python

範囲ループ(range)で厳密にインデックスを管理する

インデックスを明示的に扱いたい場合は、range と len を使い、M[r][c] へアクセスします。長さ不一致があり得る場合はガードを入れます。

rows, cols = len(M), len(M[0])
for r in range(rows):
    for c in range(cols):
        print(r, c, M[r][c])
Python

行ごとに列数が異なる可能性があるなら、行の長さで回すようにします。

for r in range(len(M)):
    for c in range(len(M[r])):
        print(r, c, M[r][c])
Python

よく使う走査パターン(抽出・転置・条件・集計)

列の抽出と列方向のループ

列 c を取り出したいときは、各行から同じ列インデックスを取りに行きます。前提として列数の整合性を確認しておくと安全です。

c = 1
col = [row[c] for row in M]
for i, v in enumerate(col):
    print(i, v)
Python

行列の転置を使った「列ループ」

zip とアンパックで転置すると、列を行のように回せます。列もリストで欲しい場合は整形します。

T = list(map(list, zip(*M)))   # 転置
for j, col in enumerate(T):
    print(j, sum(col))
Python

条件付き走査(スキップと早期終了)

予め軽い条件でスキップし、必要な場面では break を使って早期終了します。これだけで大きなマトリクスでも体感速度が変わります。

target = 5
found = None

for r, row in enumerate(M):
    if not row:      # 空行はスキップ
        continue
    for c, val in enumerate(row):
        if val == target:
            found = (r, c)
            break
    if found:
        break

print(found)
Python

行・列の集計(合計・最大・平均)

集計は「行単位」か「列単位」かを決め、内側の計算を最小化します。小さな関数に切り出すと再利用しやすくなります。

row_sums = [sum(row) for row in M]
col_sums = [sum(col) for col in zip(*M)]
print(row_sums, col_sums)
Python

近傍走査と境界ガード(グリッドで強い書き方)

上下左右の近傍(4近傍)を安全に回す

境界チェックを先に入れ、ループ内の条件分岐を減らすと読みやすくなります。

H, W = len(M), len(M[0])

def neighbors4(r, c):
    for dr, dc in ((-1,0), (1,0), (0,-1), (0,1)):
        nr, nc = r + dr, c + dc
        if 0 <= nr < H and 0 <= nc < W:
            yield nr, nc, M[nr][nc]

for r in range(H):
    for c in range(W):
        vals = [v for _, _, v in neighbors4(r, c)]
        print(r, c, vals)
Python

斜めも含めた近傍(8近傍)

相対位置のリストを事前に定義し、同じ境界ガードで回せば拡張が容易です。

OFFSETS8 = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if not (dr == 0 and dc == 0)]

def neighbors8(r, c):
    for dr, dc in OFFSETS8:
        nr, nc = r + dr, c + dc
        if 0 <= nr < H and 0 <= nc < W:
            yield nr, nc, M[nr][nc]
Python

安全性と性能の勘所(整合チェック・コピー・前取り)

行長の整合性をチェックして事故を防ぐ

転置や列操作をする前に、全行の列数が揃っているか簡単に確認します。これだけで例外を多く回避できます。

def is_rectangular(M) -> bool:
    return len({len(row) for row in M}) <= 1

assert is_rectangular(M), "行ごとに列数が異なるため、転置・列操作が安全に行えません"
Python

走査中の破壊的変更を避ける(必要なら防御的コピー)

ループで参照している構造を同時に書き換えると、意図しない動作や例外の原因になります。編集が必要なら、対象階層だけコピーしてから操作します。

safe = [row[:] for row in M]   # 行単位で独立
for r, row in enumerate(safe):
    for c, val in enumerate(row):
        safe[r][c] = val * 2
Python

重い参照や関数の前取りで軽くする

ループ内で何度も呼ぶ関数や属性参照は、外で変数に束縛してから使うと高速です。キー計算や検証も先に済ませます。

lower = str.lower
normalized = [[lower(str(x)) for x in row] for row in M]
Python

巨大データは「作らず流す」(ジェネレータで遅延処理)

すべての結果リストが不要なら、ジェネレータで流し先へ直接渡します。合計や最大のような集計に向いています。

def iter_values(M):
    for row in M:
        for v in row:
            yield v

total = sum(iter_values(M))
Python

例題で身につける(定番から一歩先まで)

例題1:二重ループで条件付き抽出(奇数だけ)

M = [[1,2,3],[4,5,6],[7,8,9]]
odds = []
for r, row in enumerate(M):
    for c, v in enumerate(row):
        if v % 2 == 1:
            odds.append((r, c, v))
print(odds)
Python

例題2:行フィルタと列選択(ヘッダ付き表)

rows = [
    ["name","score","age"],
    ["alice",85,25],
    ["bob",92,30],
    ["cara",70,28],
]
header, data = rows[0], rows[1:]
score_i = header.index("score")
name_i  = header.index("name")
age_i   = header.index("age")

result = [[row[name_i], row[age_i]] for row in data if row[score_i] >= 80]
print(result)  # [['alice',25], ['bob',30]]
Python

例題3:列の追加(整合チェックつき)

def add_column(M, values):
    assert len(M) == len(values), "行数と追加列の長さが一致していません"
    for row, v in zip(M, values):
        row.append(v)
    return M

M = [[1,2],[3,4],[5,6]]
add_column(M, [10,20,30])
print(M)  # [[1,2,10],[3,4,20],[5,6,30]]
Python

例題4:近傍合計(4近傍の合計を新しい行列に)

M = [[1,2,3],
     [4,5,6],
     [7,8,9]]
H, W = len(M), len(M[0])

def neighbors4_sum(r, c):
    s = 0
    for dr, dc in ((-1,0),(1,0),(0,-1),(0,1)):
        nr, nc = r + dr, c + dc
        if 0 <= nr < H and 0 <= nc < W:
            s += M[nr][nc]
    return s

out = [[neighbors4_sum(r, c) for c in range(W)] for r in range(H)]
print(out)
Python

まとめ

二次元リストのループは「外側=行、内側=列」という基本形に、enumerate でのインデックス取得、範囲ループでの厳密アクセス、軽い条件の早期スキップ、break による早期終了を組み合わせると読みやすく堅牢になります。列抽出や転置、条件付き抽出、集計、近傍走査といった定番パターンを身につけ、整合チェック・防御的コピー・前取りによる高速化で安全性と性能を両立させましょう。巨大データは「作らず流す」発想も有効です。まずは小さな表で手を動かし、失敗しやすいところ(行長不一致、走査中の破壊的変更)を体で覚えるのが上達の近道です。

タイトルとURLをコピーしました