閉じる

【Python】複雑なforループは卒業!itertoolsで多重ループ整理

深くネストしたforループは、すぐに読みづらく保守もしづらくなります。

標準ライブラリのitertoolsを使えば、同じ処理をより短く、そして省メモリで書けます。

本記事ではproductchain、順列・組み合わせ関数を使って、多重ループを整理する実践的な方法を初心者向けに丁寧に解説します。

Python初心者向け: ネストしたforループをitertoolsで整理

多重ループが読みにくい理由

入れ子のforが増えるほど、インデントが深くなり条件分岐やbreakの挙動が追いづらくなります。

変数名の衝突、抜け漏れやすいcontinue、意図しない計算量の爆発など、可読性・保守性・性能のすべてに悪影響が出やすい点が問題です。

まずは典型例を見てみます。

Python
# ネストが深いと意図が埋もれやすい例
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のネストを薄くできます。

特にproductchainは、多重ループの意図を「直読」できる形にするため、読みやすさに大きく寄与します。

以下は代表的な置き換えイメージです。

用途従来の書き方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バグ減・表現力向上

productで多重ループを1行に

2重ループはproduct(A, B)で置き換え

itertools.productは、複数のイテラブルの直積(総当たり)を生成します。

2重ループは次のように書き換えられます。

Python
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

複数のイテラブルをそのまま渡せます。

ネストの深さがそのまま引数の数になるだけです。

Python
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行で書けます。

Python
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で全探索を止められます。

Python
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を使うと安全です。

chainで入れ子データを平坦化

リストのリストはchain.from_iterableで1ループ

「リストのリスト」を2重ループで回す代わりに、chain.from_iterableで平坦化してから処理すると読みやすくなります。

Python
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]

大きな入れ子をすべてリスト化せずに順次処理できるため、省メモリで扱えます。

複数シーケンスを順につなぐならchain

複数の列を1つの流れとして順に処理する場合はchainが便利です。

連結のために新しいリストを作らないので無駄なメモリを使いません。

Python
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を使います。

Python
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、順列・組み合わせはpermutationscombinations系を使うのが定石です。

これらはいずれもイテレータを返すため、巨大データでも省メモリで扱えるのが魅力です。

一方で、総当たりの組合せ数は容易に爆発します。

条件で早めに絞る、breaknextで早期終了する、安易にlist(...)で全展開しないといった基本を守りましょう。

今日から多重ループをitertoolsで整理して、短く、速く、読みやすいコードに置き換えていってください。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!