深くネストしたforループは、すぐに読みづらく保守もしづらくなります。
標準ライブラリのitertools
を使えば、同じ処理をより短く、そして省メモリで書けます。
本記事ではproduct
やchain
、順列・組み合わせ関数を使って、多重ループを整理する実践的な方法を初心者向けに丁寧に解説します。
Python初心者向け: ネストしたforループをitertoolsで整理
多重ループが読みにくい理由
入れ子のfor
が増えるほど、インデントが深くなり条件分岐やbreak
の挙動が追いづらくなります。
変数名の衝突、抜け漏れやすいcontinue
、意図しない計算量の爆発など、可読性・保守性・性能のすべてに悪影響が出やすい点が問題です。
まずは典型例を見てみます。
# ネストが深いと意図が埋もれやすい例
A = [1, 2, 3]
B = [10, 20, 30]
results = []
for a in A:
for b in B:
# 条件が増えると読み解きに時間がかかる
if (a * b) % 2 == 0 and (a + b) <= 25:
results.append((a, b))
print("ヒットしたペア:", results)
ヒットしたペア: [(1, 10), (2, 10)]
この例程度ならまだしも、3重・4重になると「何を全探索しているのか」「どの条件がどの段階で効いているのか」が見えにくくなります。
itertoolsを使うメリット(シンプル・省メモリ)
itertools
はイテレータ(逐次生成)を返す関数群です。
必要になった要素だけを1つずつ生成するので省メモリで、またfor
のネストを薄くできます。
特にproduct
とchain
は、多重ループの意図を「直読」できる形にするため、読みやすさに大きく寄与します。
以下は代表的な置き換えイメージです。
用途 | 従来の書き方 | itertoolsでの書き方 | 主な利点 |
---|---|---|---|
直積(総当たり) | for a in A: for b in B: | for a, b in product(A, B): | ネスト解消・意図が明確 |
グリッド走査 | for i in range(H): for j in range(W): | for i, j in product(range(H), range(W)): | 1行で座標生成 |
平坦化 | 入れ子を2重で回す | chain.from_iterable(nested) | シンプル・省メモリ |
順列・組み合わせ | 手書きの多重ループ | permutations / combinations | バグ減・表現力向上 |
- 関連記事:はじめてのfor文: リスト処理の書き方とコツ
- 関連記事:ジェネレータの基本と使い方: 遅延評価で省メモリ処理
- 関連記事:イテレーションを効率化 (chain,islice,permutations)の使い方
productで多重ループを1行に
2重ループはproduct(A, B)で置き換え
itertools.product
は、複数のイテラブルの直積(総当たり)を生成します。
2重ループは次のように書き換えられます。
from itertools import product
A = [1, 2, 3]
B = [10, 20, 30]
# 条件は内包表記やifで後段にまとめられるのでスッキリ
results = [(a, b) for a, b in product(A, B) if (a * b) % 2 == 0 and (a + b) <= 25]
print("productを使った結果:", results)
productを使った結果: [(1, 10), (2, 10)]
ループが1段にまとまるため、「何を総当たりしているのか」が一目で分かります。
3重以上もproduct(A, B, C)でOK
複数のイテラブルをそのまま渡せます。
ネストの深さがそのまま引数の数になるだけです。
from itertools import product
A = [0, 1]
B = ["a", "b"]
C = [True, False]
# 2 * 2 * 2 = 8通りを順に生成 (イテレータなので必要な分だけ取り出される)
triples = list(product(A, B, C))
print("3集合の直積:", triples)
print("通り数:", len(triples))
3集合の直積: [(0, 'a', True), (0, 'a', False), (0, 'b', True), (0, 'b', False), (1, 'a', True), (1, 'a', False), (1, 'b', True), (1, 'b', False)]
通り数: 8
range同士のグリッド走査はproduct(range(n), range(m))
グリッドの座標生成は典型的な2重ループです。
product
なら1行で書けます。
from itertools import product
H, W = 2, 3 # 高さ2, 幅3のグリッド
coords = list(product(range(H), range(W)))
print("座標一覧:", coords)
# 実際の走査も1重ループで実行
for i, j in product(range(H), range(W)):
print(f"セル({i}, {j})を処理中")
座標一覧: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
セル(0, 0)を処理中
セル(0, 1)を処理中
セル(0, 2)を処理中
セル(1, 0)を処理中
セル(1, 1)を処理中
セル(1, 2)を処理中
大量の総当たりに注意(計算量・breakの扱い)
直積は組合せ数が掛け算で増えるため、入力が少し大きくなるだけで爆発的に増えます。
必要な条件で早めに絞り込み、必要なら早期終了しましょう。
またbreak
の挙動にも違いがあります。
従来のネストではbreak
は最内層しか抜けませんが、product
で1重にした場合はbreak
で全探索を止められます。
from itertools import product
A = [1, 2, 3]
B = [10, 20, 30]
target = (2, 20)
# 1) 従来の2重ループでのbreak (内側しか抜けない例)
visited_nested = []
for a in A:
for b in B:
visited_nested.append((a, b))
if (a, b) == target:
break # ここでは内側だけを抜ける
# 期待に反して、a=3 のループが続行される
print("従来の2重ループで訪れた順序:", visited_nested)
# 2) productでのbreak (ループ自体が1つなので全体を止められる)
visited_product = []
for a, b in product(A, B):
visited_product.append((a, b))
if (a, b) == target:
break # ここで全探索を停止できる
print("productで訪れた順序:", visited_product)
従来の2重ループで訪れた順序: [(1, 10), (1, 20), (1, 30), (2, 10), (2, 20), (3, 10), (3, 20), (3, 30)]
productで訪れた順序: [(1, 10), (1, 20), (1, 30), (2, 10), (2, 20)]
早期終了が欲しい検索処理は、product
で1重化してからbreak
またはnext
を使うと安全です。
- 関連記事:in演算子は遅い? リストとセット/辞書の計算量比較
- 関連記事:any()とall()の使い方まとめ: 複数条件を1行で判定
- 関連記事:実行時間を計測する方法まとめ(timeitとperf_counter)
chainで入れ子データを平坦化
リストのリストはchain.from_iterableで1ループ
「リストのリスト」を2重ループで回す代わりに、chain.from_iterable
で平坦化してから処理すると読みやすくなります。
from itertools import chain
nested = [[1, 2], [3], [], [4, 5]]
# 2重ループの代わりに、一列のストリームとして処理できる
flat = list(chain.from_iterable(nested))
print("平坦化:", flat)
# 例えば合計やフィルタも1重ループの感覚で
total = sum(chain.from_iterable(nested))
evens = [x for x in chain.from_iterable(nested) if x % 2 == 0]
print("合計:", total)
print("偶数のみ:", evens)
平坦化: [1, 2, 3, 4, 5]
合計: 15
偶数のみ: [2, 4]
大きな入れ子をすべてリスト化せずに順次処理できるため、省メモリで扱えます。
- itertools.chain / chain.from_iterable(公式)
- 関連記事:ゼロからわかるPythonリスト(list)の作り方と使いどころ
- 関連記事:setでリストの重複を一瞬で削除する方法と注意点
複数シーケンスを順につなぐならchain
複数の列を1つの流れとして順に処理する場合はchain
が便利です。
連結のために新しいリストを作らないので無駄なメモリを使いません。
from itertools import chain
seq1 = [1, 2]
seq2 = range(3, 5) # 3, 4
seq3 = ("x", "y")
joined = list(chain(seq1, seq2, seq3))
print("順につないだ結果:", joined)
順につないだ結果: [1, 2, 3, 4, 'x', 'y']
組み合わせ探索は専用関数でネスト不要
permutationsで順列ループを簡潔に
順序を区別して要素を並べる「順列」はpermutations
を使います。
多重ループを手で書く必要はありません。
combinationsで組み合わせをシンプルに
順序を区別しない「組み合わせ」はcombinations
で表現できます。
重複は含みません。
combinations_with_replacementで重複ありの組み合わせ
同じ要素を複数回選ぶことを許す場合はcombinations_with_replacement
を使います。
from itertools import permutations, combinations, combinations_with_replacement
items = ["A", "B", "C"]
# 1) 順列: 順序を区別 (r=2 の例)
perms = list(permutations(items, 2))
print("permutations(items, 2):", perms)
# 2) 組み合わせ: 順序を区別しない (r=2 の例)
combs = list(combinations(items, 2))
print("combinations(items, 2):", combs)
# 3) 重複あり組み合わせ: 同じ要素を複数回選べる (r=2 の例)
combs_wr = list(combinations_with_replacement(items, 2))
print("combinations_with_replacement(items, 2):", combs_wr)
permutations(items, 2): [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
combinations(items, 2): [('A', 'B'), ('A', 'C'), ('B', 'C')]
combinations_with_replacement(items, 2): [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
自前の多重ループで順序や重複の扱いを間違えるリスクを減らせる点が大きな利点です。
必要な長さr
を指定するだけで、意図どおりの列挙ができます。
まとめ
多重ループはitertools
で「1重化」すると一気に読みやすくなります。
直積はproduct
、入れ子の平坦化はchain.from_iterable
、順列・組み合わせはpermutations
やcombinations
系を使うのが定石です。
これらはいずれもイテレータを返すため、巨大データでも省メモリで扱えるのが魅力です。
一方で、総当たりの組合せ数は容易に爆発します。
条件で早めに絞る、break
やnext
で早期終了する、安易にlist(...)
で全展開しないといった基本を守りましょう。
今日から多重ループをitertools
で整理して、短く、速く、読みやすいコードに置き換えていってください。