sec03 - Python 二重ループの完全ガイド:ネスト・break/continue・無限ループ対策
スポンサーリンク

1. 2重ループの基本

まずは、2重ループの基本構造を確認しましょう。Pythonでは、forwhileを入れ子にして記述することで、1つのループの中でさらに繰り返し処理を行うことができます。これを「ループのネスト(入れ子)」と呼びます。

次の例では、3×3の表を作るように、外側ループと内側ループを組み合わせて出力します。

for i in range(3):
    for j in range(3):
        print(f'(i[{i}], j[{j}])', end=' ')

    print()

出力結果:

(i[0], j[0]) (i[0], j[1]) (i[0], j[2]) 
(i[1], j[0]) (i[1], j[1]) (i[1], j[2])
(i[2], j[0]) (i[2], j[1]) (i[2], j[2])

このとき、ijの変化を表で整理すると、次のようになります。

外側ループ { i } の変化内側ループ { j } の変化
00 → 1 → 2
10 → 1 → 2
20 → 1 → 2

つまり、外側ループの1回の繰り返しごとに、内側ループがすべての回数を実行しています。外側ループの先頭に処理が戻り、再度内側ループ(for j in range(3):)が実行される時には、range(3)の処理は最初からスタートします。(0 → 1 → 2と処理が進みます。)

次に、listを使った2重ループの例を見てみましょう。

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

for row in matrix:
    for value in row:
        print(value, end=' ')
    print()

出力結果:

1 2 3
4 5 6
7 8 9

処理の流れを表で見てみましょう。

row の値value の変化
[1, 2, 3]1 → 2 → 3
[4, 5, 6]4 → 5 → 6
[7, 8, 9]7 → 8 → 9

外側ループでは行(row)を1つずつ取り出し、内側ループではその行の中の要素を1つずつ処理しています。

次に、whileループを使ったネストの例です。

i = 0
while i < 3:
    j = 0
    while j < 3:
        print(i, j)
        j += 1
    i += 1

whileループの場合、内側ループに対応したカウンター(変数j)も用意するなど、無限ループにならないように配慮する必要があります。

これまで見てきたように、forforのネストや、whilewhileのネスト以外にも、外側ループと内側ループの性質に基づいて、固定回数の反復を行うループとそうでないループを組み合わせたい場合は、forwhileを組み合わせることができます。

2. ループ制御の基本(break / continue)

2重ループでは、breakcontinueを使って繰り返しを制御することができます。ただし、それらが「内側ループ」か「外側ループ」に作用するかを理解しておくことが大切です。

2.1 breakの挙動

次の例では、内側ループでbreakを使っています。

for i in range(3):
    for j in range(3):
        if j == 1:
            break
        print(i, j)

出力結果:

0 0
1 0
2 0

変数j1の時にbreakするので(breakするとprint(i, j)は実行されない)、変数j0の時のprintのみ出力されます。breakは内側ループだけを終了させ、外側ループは続行します。(変数i0 → 1 → 2と処理が進みます。)

次の例は、外側ループでbreakを使っています。

for i in range(3):
    if i == 1:
        break
    for j in range(3):
        print(i, j)

出力結果:

0 0
0 1
0 2

変数i1の時にbreakするので、変数i0の時のprintのみ出力されます。外側ループでbreakされると、ループ全体が終了します。

2.2 continueの挙動

continueを使うと、現在のループの1回分をスキップします。

for i in range(3):
    for j in range(3):
        if j == 1:
            continue
        print(i, j)

出力結果:

0 0
0 2
1 0
1 2
2 0
2 2

内側ループでcontinueされた場合は、その回のprint(i, j)は実行されずにfor j in range(3):の先頭に処理が戻ります。ですので、変数j1の時以外の情報が出力されています。

次のループは、外側ループでcontinueされています。

for i in range(3):
    if i == 1:
        continue
    for j in range(3):
        print(i, j)

出力結果:

0 0
0 1
0 2
2 0
2 1
2 2

変数i1の時にcontinueにより外側ループの先頭に戻るので、変数i1以外の時のみprint出力されます。

スポンサーリンク

3. 変数スコープと値の更新

ネストされたループでは、変数のスコープ(有効範囲)にも注意が必要です。Pythonでは、ループ変数は同じスコープ(関数内やスクリプト全体)に存在します。そのため、同じスコープの中で使用される他の変数の値を誤って上書きしたり、予期せぬ値を参照しないように気を付けましょう。

(スコープについては専用のレクチャーで学習します。)

for i in range(2):
    for j in range(3):
        print(i, j)
print('ループ後のi:', i)
print('ループ後のj:', j)

出力結果:

0 0
0 1
0 2
1 0
1 1
1 2
ループ後のi: 1
ループ後のj: 2

このように、ループが終了したあとでも、変数iとjは残っています。予期せぬ値を参照してしまうミスを防ぐため、スコープを意識することが重要です。

4. インデントと可読性、ネストの落とし穴

2重ループでは、インデントが1段深くなるため、可読性が下がりやすくなります。適切な空行やコメントを入れて、構造を明確にしましょう。

また、更にネストさせて3重ループ、4重ループと増やすこともできますが、ネストさせればさせるほど可読性が下がり、バグが紛れる可能性も高まります。大量の繰り返し処理が必要な場合は、機能ごとに関数に分けて管理するなど、ネストが深くなりすぎないように工夫してコードを書くようにしましょう。

(関数については専用のレクチャーで学習します。)

for i in range(2):
    print(f'iのループ開始: {i}')
    for j in range(2):
        print(f'  jの処理: {j}')
        for k in range(3):
            if k == 2:
                continue
            print(f'    kの処理: {k}')
    print(f'i({i})のループ終了')

出力結果:

iのループ開始: 0
  jの処理: 0
    kの処理: 0
    kの処理: 1
  jの処理: 1
    kの処理: 0
    kの処理: 1
i(0)のループ終了
iのループ開始: 1
  jの処理: 0
    kの処理: 0
    kの処理: 1
  jの処理: 1
    kの処理: 0
    kの処理: 1
i(1)のループ終了

5. 無限ループと終了条件の確認

whileループをネストする場合、終了条件を誤ると無限ループに陥ることがあります。

i = 0
while i < 3:
    j = 0
    while j < 3:
        print(i, j)
        # 'j += 1' を忘れてしまうと無限ループになってしまう
    i += 1  # 'i += 1'も忘れずに

変数ijを両方とも更新しないと、永遠に同じ処理が繰り返されてしまうため注意が必要です。

スポンサーリンク