0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

演習形式で学ぶPythonプログラミング vol.12 ~ループ処理におけるbreakとcontinue~

Last updated at Posted at 2025-05-01

まえがき

Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。

今回は「ループ処理における breakcontinue」について扱います。それでは始めましょう!

Q.12-1

while True: を使用した無限ループで、条件を満たさない値を continue によってスキップし、満たした瞬間に break で脱出する複合構造を設計せよ。

問題背景

【1】while True: による無限ループ

常に True が評価される限り、無限ループは続く。ループ内で特定の条件が満たされた場合に break を使ってループを終了、あるいは条件が満たされない場合には continue を使って、ループ内の残りの処理をスキップして次のイテレーションを開始することができる。
continue:現在のループの残りの処理をスキップして、次のループに進む。
break:ループを完全に終了させる。

解答例とコードによる実践

# 【1】無限ループの開始
while True:
    # 【2】ユーザーから入力を受け取る
    user_input = input("1から10の数字を入力してください(終了するには 'exit' と入力):")
    
    # 【3】'exit' と入力された場合、ループを終了
    if user_input == "exit":
        print("プログラムを終了します。")
        break  # 【4】ループを終了
    
    # 【5】入力が数字に変換できるかどうかを確認
    try:
        num = int(user_input)  # 【6】入力された文字列を整数に変換
    except ValueError:  # 【7】入力が整数に変換できなかった場合
        print("無効な入力です。数字を入力してください。")
        continue  # 【8】次のループに進む(再入力を求める)
    
    # 【9】入力された数字が1から10の範囲かどうかをチェック
    if num < 1 or num > 10:
        print(f"{num} は1から10の範囲外です。再度入力してください。")
        continue  # 【10】範囲外の入力をスキップし、再入力を求める
    
    # 【11】条件を満たした場合にループを終了
    print(f"{num} は有効な数字です。ループを終了します。")
    break  # 【12】ループを終了

# 【13】ループ終了後のメッセージ
print("プログラムが終了しました。")
実行結果
# 実行例1: 数字が1から10の範囲内で終了する場合
1から10の数字を入力してください(終了するには 'exit' と入力):5
5 は有効な数字です。ループを終了します。
プログラムが終了しました。

# 実行例2: exit と入力して終了する場合
1から10の数字を入力してください(終了するには 'exit' と入力):exit
プログラムを終了します。
プログラムが終了しました。

# 実行例3: 範囲外の値を入力した場合
1から10の数字を入力してください(終了するには 'exit' と入力):15
15 は1から10の範囲外です。再度入力してください。
1から10の数字を入力してください(終了するには 'exit' と入力):3
3 は有効な数字です。ループを終了します。
プログラムが終了しました。

# 実行例4: 無効な入力をした場合
1から10の数字を入力してください(終了するには 'exit' と入力):abc
無効な入力です。数字を入力してください。
1から10の数字を入力してください(終了するには 'exit' と入力):8
8 は有効な数字です。ループを終了します。
プログラムが終了しました。

本問題のポイントは以下

continue の使い方:無効な入力や範囲外の入力に対して continue を使うことで、コードの意図が明確になる。continue は次のイテレーションに進むため、処理の流れが明確になる。
・ エラーハンドリング:try-except を使って、数値以外の入力を適切に処理している。これにより、ユーザーが無効な入力をした場合にも、プログラムがクラッシュすることなく次に進むことができる。
・ 条件チェックの簡潔化:範囲外のチェックをシンプルに行い、範囲外の入力をすぐにスキップできるようにしている。

Q.12-2

pop() によってキューの先頭から処理していく中で、処理対象が None になった時点で break し、ログを残す設計を記述せよ。

問題背景

【1】キュー

キューは「先入れ先出し(FIFO)」のデータ構造であり、データを追加する際は、後ろから追加したり、取り出す際には前から取り出す。実際の処理シーケンスでは、「タスク管理」や「ジョブキュー」など、順番に処理を行いたい場合に使用される。Pythonのリストは、append() メソッドで後ろからデータを追加し、pop(0) で先頭のデータを取り出すことにより、キューとして利用することが出来る。

【2】pop(0) メソッドの利用

リストの pop(0) メソッドは、指定したインデックスの要素をリストから取り出して返す。引数なしで pop() を呼び出すと、リストの末尾から要素を取り出すが、pop(0) を使うことでリストの先頭から要素を取り出すことができる。これにより、キューの先頭から順番に要素を処理することができる。

queue = ['task1', 'task2', 'task3']
item = queue.pop(0)  # 'task1' が取り出される
print(item)  # task1
print(queue)  # ['task2', 'task3']

この例では、task1 が取り出され、残りの要素は task2task3 である。

【3】 None を用いた終了条件

Pythonにおいて、None は「何もない」ことを示すオブジェクトで、特に「終了」を示すマーカーとしてよく用いられる。キューの処理中に None が現れた場合にループを終了させるという設計にすることで、処理の途中で特定の条件を満たしたときに即座に終了することが出来る。
以下は、キューに None を挿入することで終了条件を設定し、pop(0) で取り出した際に None が現れた場合にループを終了させ、終了理由を表示する。

queue = ['task1', 'task2', None, 'task3']

while queue:
    item = queue.pop(0)
    if item is None:
        print("処理対象が None だったため、ループを終了します。")
        break  # ループ終了
    print(f"処理中: {item}")

【4】logging モジュールの利用

logging モジュールは、Pythonの標準ライブラリで、プログラムの実行中に発生したイベントを記録するために使用される。print() の代わりに logging を使うことで、デバッグや運用の際に重要な情報を記録し、後からログを確認できるようにすることができる。
logging の使い方として、以下のようにログの設定を行い、ログメッセージを出力することができる。

import logging

# ログの設定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# ログに情報を記録
logging.info("これは情報メッセージです。")
logging.warning("これは警告メッセージです。")
logging.error("これはエラーメッセージです。")

上記のコードでは、INFOWARNINGERROR といった異なるログレベルでメッセージを記録することができる。これにより、プログラムの実行状況やエラーを適切に記録でき、後からログを分析することが容易になる。

【5】 break の使用

break 文は、ループを即座に終了させるために使用される。キューの処理中に特定の条件(例えば、None を検出した場合)でループを終了したい場合に有効である。break を使うことで、必要ないループの繰り返しを防ぐことができる。

解答例とコードによる実践

# 【1】logging モジュールを読み込む
import logging

# 【2】ログの設定を行う(INFO レベル以上を表示)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】キューを定義(先頭から処理していく)
queue = ['task1', 'task2', None, 'task3']  # Noneが現れたら処理を終了する

# 【4】無限ループの開始(キューが空になるまで続ける)
while queue:
    # 【5】キューの先頭から要素を取り出す(FIFO)
    item = queue.pop(0)

    # 【6】取り出した要素が None なら終了
    if item is None:
        # 【7】終了理由をログに記録
        logging.info("処理対象が None だったため、キュー処理を終了します。")
        break  # 【8】ループを終了

    # 【9】処理対象が存在する場合は表示(実際の処理に相当)
    print(f"処理中: {item}")

# 【10】ループ後の終了メッセージ
print("すべての処理が終了しました。")
実行結果
# 実行例1: None が現れた場合
処理中: task1
処理中: task2
2025-05-01 18:12:34,123 - INFO - 処理対象が None だったため、キュー処理を終了します。
すべての処理が終了しました。

# 実行例2: キューに None がなく、全て処理される場合
処理中: task1
処理中: task2
処理中: task3
すべての処理が終了しました。

【補足】

【1】 ログをファイルにも使いたい場合

logging.basicConfig() の設定を変更した場合、ログの出力先が標準出力(コンソール)からファイル queue.log に変更されるため、ログはファイルに記録され、標準出力には表示されない。そのため、コードの出力結果は先述のものに加えてログファイルに記録がなされる:

  1. 標準出力(コンソール)に表示される内容
    print(f"処理中: {item}") の部分によって、処理対象のアイテムが標準出力に表示される。具体的には、None が処理されるまでのタスクが表示される。
  2. ログファイル queue.log に記録される内容
    logging.info() によって、None を検出した時点でログがファイルに記録されます。ログファイルには、None を検出したことと、その理由が INFO レベルで記録されます。

組み込み後のコード例

import logging

# 【1】ログ出力の設定:INFOレベル以上を表示し、フォーマットも指定
logging.basicConfig(
    filename='queue.log', 
    level=logging.INFO, 
    format='%(asctime)s - %(levelname)s - %(message)s',
    filemode='w'  # 'w' にすることでファイルが毎回上書きされる。'a' で追記モード
)

# 【3】キューを定義(先頭から処理していく)
queue = ['task1', 'task2', None, 'task3']  # Noneが現れたら処理を終了する

# 【4】無限ループの開始(キューが空になるまで続ける)
while queue:
    # 【5】キューの先頭から要素を取り出す(FIFO)
    item = queue.pop(0)

    # 【6】取り出した要素が None なら終了
    if item is None:
        # 【7】終了理由をログに記録
        logging.info("処理対象が None だったため、キュー処理を終了します。")
        break  # 【8】ループを終了

    # 【9】処理対象が存在する場合は表示(実際の処理に相当)
    print(f"処理中: {item}")

# 【10】ループ後の終了メッセージ
print("すべての処理が終了しました。")
実行結果(標準出力)
処理中: task1
処理中: task2
すべての処理が終了しました。

task1task2 が順番に処理され、標準出力に表示される。None が見つかった時点で break によりループを終了し、その後 print("すべての処理が終了しました。") が表示される。
一方で、queue.log というファイルに次のような内容が記録される。

ログファイル
2025-05-01 18:12:34,123 - INFO - 処理対象が None だったため、キュー処理を終了します。

ログには、None を検出したために処理を終了した理由が INFO レベルで記録されます。asctime には処理が行われた時刻、levelname にはログレベル(INFO)、message には実際に記録されたメッセージが表示される。

重要なポイントは以下。
・ ログファイルへの出力:logging.basicConfig()filename='queue.log' により、ログメッセージが標準出力ではなく指定したファイル(queue.log)に書き込まれるようになる。filemode='w' により、毎回新しい実行ごとにログファイルが上書きされる(filemode='a' を使用すると、ファイルが追記される)。
・ ログフォーマット:format='%(asctime)s - %(levelname)s - %(message)s' により、ログにはタイムスタンプ(asctime)、ログレベル(levelname)、そしてメッセージ(message)が含まれる。この設定により、ログが時系列で管理され、エラートラッキングやデバッグが容易になる。

Q.12-3

複数のループが連携している場合に、break を使うと一部だけ処理されなくなるパターンを可視化付きで検証せよ。

問題背景

【1】 break の動作

break の目的:break 文は、ループ(forwhile)を強制的に終了させるために使用される。break が実行されると、そのループの残りの処理は無視され、次のステートメントに制御が移る。
内側のループと外側のループ:break を内側のループに使うと、その内側のループのみが終了し、外側のループには影響を与えない。これは、break が実行されるループにのみ影響を与えるから。

# 内側のループに対するbreak
for i in range(3):
    for j in range(3):
        print(f"処理中: i={i}, j={j}")
        if j == 1:
            break  # ここで内側のループのみ終了
実行結果
処理中: i=0, j=0
処理中: i=0, j=1
処理中: i=1, j=0
処理中: i=1, j=1
処理中: i=2, j=0
処理中: i=2, j=1

この例では、j == 1 の時点で内側のループが終了し、次の外側のループに進む。

【2】 内外のループの関係

外側ループと内側ループ:複数のループがネストしている場合、内側のループが終了しても外側のループはそのまま継続する。break が内側のループに対してのみ作用するため、外側のループは処理を続けることになる。
意図しない動作:内側のループが終了した時点で外側のループも終了させたければ、break を外側のループに適用する必要がある。

for i in range(3):
    for j in range(3):
        print(f"処理中: i={i}, j={j}")
        if j == 1:
            break  # 内側のループ終了
    print("内側ループが終了した後、外側ループを継続")
実行結果
処理中: i=0, j=0
処理中: i=0, j=1
内側ループが終了した後、外側ループを継続
処理中: i=1, j=0
処理中: i=1, j=1
内側ループが終了した後、外側ループを継続
処理中: i=2, j=0
処理中: i=2, j=1
内側ループが終了した後、外側ループを継続

この例では、内側のループが終了した後でも外側のループがそのまま進むことがわかる。

【3】 break による処理スキップ

一部の処理がスキップされる問題:break を使うと、内側のループでの処理が途中でスキップされる場合がある。この場合、外側のループが続いても、意図した処理が実行されないことになる。
解決方法:内側のループを終了したい場合、適切な位置で break を使い、外側のループに影響を与えないようにする。もし外側のループも終了させたい場合は、外側のループに対して break を使う。

for i in range(3):
    for j in range(3):
        if j == 1:
            print(f"i={i}, j={j}: 1に達したので内側ループを終了")
            break  # ここで内側ループのみ終了
        print(f"i={i}, j={j}: 続行")
実行結果
i=0, j=0: 続行
i=0, j=1: 1に達したので内側ループを終了
i=1, j=0: 続行
i=1, j=1: 1に達したので内側ループを終了
i=2, j=0: 続行
i=2, j=1: 1に達したので内側ループを終了

この例では、j == 1 の時点で内側のループが終了し、それ以降の処理がスキップされる。

【4】 可視化と問題点

内側のループが終了した時点で、外側のループは続行されるという点を可視化する。意図せず一部の処理がスキップされることがあり、その影響をよく理解しておく必要がある。

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

# 内外のループの動作を可視化
for row in data:
    for item in row:
        print(f"処理中: {item}")
        if item == 5:
            print("5が見つかったので内側ループを終了します。")
            break  # 内側のループのみ終了
    print("外側ループの次の行へ進みます。")
実行結果
処理中: 1
処理中: 2
処理中: 3
外側ループの次の行へ進みます。
処理中: 4
処理中: 5
5が見つかったので内側ループを終了します。
外側ループの次の行へ進みます。
処理中: 7
処理中: 8
処理中: 9
外側ループの次の行へ進みます。

この例では、5 が見つかると内側のループが終了し、その後外側のループは次の行に進むことが確認できる。break は内側のループにのみ影響を与えるため、外側のループはそのまま続行する。

解答例とコードによる実践

まず、複数のループ(内側と外側)が連携しているケースを考え、break が内側のループに対してのみ適用され、外側のループには影響を与えないことを示す。さらに、これを可視化するために、リストの処理を例として使用し、処理内容を表示する。

# 【1】処理中の状態を可視化するためのリストを定義
data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# 【2】外側のループ(行ごとの処理)
for row in data:
    # 【3】内側のループ(列ごとの処理)
    for value in row:
        print(f"処理中: {value}")  # 【4】現在処理している値を表示
        
        # 【5】特定の条件で break を使って内側のループを抜ける
        if value == 5:
            print("5が見つかったので内側ループを終了します。")
            break  # 【6】内側のループを終了

    # 【7】外側のループはそのまま続きます
    print("外側ループの次の行へ進みます。\n")

# 【8】処理後の終了メッセージ
print("すべての処理が終了しました。")
実行結果
処理中: 1
処理中: 2
処理中: 3
外側ループの次の行へ進みます。

処理中: 4
処理中: 5
5が見つかったので内側ループを終了します。
外側ループの次の行へ進みます。

処理中: 7
処理中: 8
処理中: 9
外側ループの次の行へ進みます。

すべての処理が終了しました。

Q.12-4

ループを break する条件が複雑な場合に、構造的に if を関数化することで読みやすさを高める設計例を記述せよ

問題背景

【1】 if 条件が複雑になる問題

Pythonでループ処理をしているとき、if 条件が複雑になり、コードの読みやすさが著しく低下する場合がある。特に andor、ネストした条件分岐、型判定、文字列や数値の多条件チェックが混在すると、可読性と保守性が極端に落ちる。
例えば、以下のような if 条件は一見して意図を把握しにくく、バグの温床となる。

if (x > 0 and x < 10) or (y == 'exit' and len(z) > 3) or some_flag is True:
    break

【2】 関数化による可読性の向上

上記のような複雑な条件は、意味のある名前を持つ関数として切り出すことで、コードの意図が明確になる。関数化により、次のような利点が得られる:
・ 条件の意味を関数名で表現できる。
・ 条件のテストや再利用が容易になる。
・ コード全体の可読性と保守性が向上する。

def should_terminate(x, y, z, some_flag):
    return (0 < x < 10) or (y == 'exit' and len(z) > 3) or some_flag is True

if should_terminate(x, y, z, some_flag):
    break

【3】 Pythonicな設計思想

Pythonにおける「明示は黙示に勝る(Explicit is better than implicit)」という原則に従えば、複雑な条件は明示的に名前付き関数として表現すべきである。これにより、コードの意図を人間が読み取りやすくなり、Pythonic(=明快で読みやすい)な設計となる。

解答例とコードによる実践

以下のコードでは、「ユーザーの入力が特定の終了条件を満たしたときにループを終了する」処理を、関数化された条件判定を通して設計する。

# 【1】logging モジュールを読み込む(ログ記録のため)
import logging

# 【2】ログ設定:INFOレベル以上を表示し、フォーマットを指定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】複雑な終了条件を判定する関数を定義
def should_terminate(user_input, trial_count, forbidden_words):
    # 【4】終了条件:試行回数が5回以上 or 入力が禁止語に含まれる or 入力が 'exit'
    return (
        trial_count >= 5 or
        user_input.lower() == 'exit' or
        user_input in forbidden_words
    )

# 【5】処理に使う変数を初期化
trial_count = 0  # 試行回数
forbidden_words = {'stop', 'quit', 'halt'}  # 禁止語

# 【6】無限ループの開始
while True:
    # 【7】ユーザー入力を受け取る
    user_input = input("入力してください('exit' または禁止語で終了): ")
    trial_count += 1  # 【8】試行回数を更新

    # 【9】関数で終了条件を判定
    if should_terminate(user_input, trial_count, forbidden_words):
        # 【10】ログに終了理由を記録
        logging.info(f"終了条件を満たしたため、ループを終了します(入力: {user_input}, 試行: {trial_count}回)")
        break  # 【11】ループを終了

    # 【12】入力が有効な場合の処理(表示だけ)
    print(f"入力内容: {user_input}{trial_count}回目)")

# 【13】終了後のメッセージを表示
print("プログラムを終了しました。")
実行結果
# 実行パターン1:禁止語 quit を入力
入力してください('exit' または禁止語で終了): hello
入力内容: hello(1回目)
入力してください('exit' または禁止語で終了): quit
2025-05-01 18:15:01,123 - INFO - 終了条件を満たしたため、ループを終了します(入力: quit, 試行: 2回)
プログラムを終了しました。

# 実行パターン2:5回入力して終了
入力してください('exit' または禁止語で終了): a
入力内容: a(1回目)
...
入力してください('exit' または禁止語で終了): e
2025-05-01 18:16:45,789 - INFO - 終了条件を満たしたため、ループを終了します(入力: e, 試行: 5回)
プログラムを終了しました。

Q.12-5

入れ子のループに return を使用した場合と break によって明示的な脱出を行った場合の挙動の差異を説明せよ。

問題背景

【1】 return の基本的な挙動

return は、現在の関数全体から即座に脱出し、戻り値を返す構文である。関数内のループにおいて return が実行されると、その関数のすべての処理が即座に終了する。

def func():
    for i in range(3):
        for j in range(3):
            if j == 1:
                return  # 関数全体が終了する
            print(i, j)
    print("これは表示されない")

func()
実行結果
0 0

このように、return により関数自体が終了してしまうため、以降のループも関数本体もすべてスキップされる。

【2】 breakreturn の構造的な違い

項目 break return
対象 現在のループのみを終了 関数そのものを終了
多重ループの脱出 1段階のみ抜ける すべてのループと関数の実行を止める
後続処理 関数内のループや文が続行する可能性がある すべての後続文が実行されない
戻り値 無し(明示的に渡せない) 明示的な値を返すことができる
適用例 条件付きのループ終了に適する 条件付きで処理を打ち切りたい場合に適す

解答例とコードによる実践

以下に、break を使った場合と return を使った場合の構造的差異を実演するコードを提示する。各行に実行順序と初心者向け解説を記載する。

# 【1】breakを使った例とreturnを使った例を関数として比較する

# 【2】breakで内側のループのみを終了する例
def break_example():
    # 【3】外側のループを3回繰り返す
    for i in range(3):
        # 【4】内側のループを3回繰り返す
        for j in range(3):
            # 【5】条件に達したら内側のループを終了
            if j == 1:
                print(f"break適用: i={i}, j={j} → 内側ループを終了")  # 【6】
                break
            print(f"break適用: i={i}, j={j}")  # 【7】
    # 【8】ループ後にも処理が継続
    print("break_example終了")  # 【9】

# 【10】returnで関数自体を終了する例
def return_example():
    # 【11】外側のループを3回繰り返す
    for i in range(3):
        # 【12】内側のループを3回繰り返す
        for j in range(3):
            # 【13】条件に達したら関数全体を終了
            if j == 1:
                print(f"return適用: i={i}, j={j} → 関数を即時終了")  # 【14】
                return
            print(f"return適用: i={i}, j={j}")  # 【15】
    # 【16】この行は実行されない
    print("return_example終了")  # 【17】

# 【18】関数の実行
print("【breakの例】")
break_example()

print("\n【returnの例】")
return_example()
実行結果
【breakの例】
break適用: i=0, j=0
break適用: i=0, j=1 → 内側ループを終了
break適用: i=1, j=0
break適用: i=1, j=1 → 内側ループを終了
break適用: i=2, j=0
break適用: i=2, j=1 → 内側ループを終了
break_example終了

【returnの例】
return適用: i=0, j=0
return適用: i=0, j=1 → 関数を即時終了

本問のポイントは以下。
break はループ構造の一時的終了に使用すべきであり、外側の処理を継続したい場合に適している。
return は関数自体を終了させたい場合に使うべきであり、より大きな構造を即時停止する用途に適している。
・ 入れ子ループで脱出構造が必要な場合には、どの範囲を終了させたいのか(ループ単位か関数単位か)を明確に区別し、適切な文法を用いるべきである。
このように、breakreturn の違いを構造的に理解することは、複雑な制御フローの正確な実装に不可欠である。

Q.12-6

continue を含むループ処理で、処理済み件数とスキップ件数を別カウントし、ログ出力付きで管理する設計を記述せよ。

問題背景

【1】continue の基本動作

continue 文は、ループ内の現在の反復処理の残り部分をスキップし、次のイテレーションへ進むために使用される。
・ 条件に基づいて処理を飛ばす際に便利だが、その下にある処理(たとえば loggingappendprint など)は実行されないため、意図せずログ記録が抜け落ちることがある。

for x in range(5):
    if x % 2 == 0:
        continue
    print(x)
# 1, 3
# 偶数は continue によって print() がスキップされている

continue の副作用とログ記録のスキップ問題

・ 実務では「処理しなかった理由」や「スキップされた対象」もログに残すことが求められる。しかし、continue が早期にループの流れを変更することで、ログが記録されないままスキップされる副作用が生じる。
この場合、continue の前にログ記録処理を挿入するのが解決方針の一つである。continue を使う前に、スキップの理由を明示的にログ出力する設計が望ましい。ログ出力後に continue を使えば、スキップの理由も確実にログに記録される。
以下はこの場合の例である。
e.g.1) 不適切な例:ログが記録されない

# 【1】ログ機能を有効化
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【2】対象データを定義
data = ["task1", None, "task2", "", "task3"]

# 【3】ループ処理開始
for item in data:
    # 【4】None または 空文字はスキップ(ログ記録なし)
    if not item:
        continue  # 【5】ログを残さずスキップされてしまう
    
    # 【6】有効なデータについて処理し、ログ記録
    logging.info(f"{item} を処理中です。")
実行結果
2025-05-01 19:01:23,456 - INFO - task1 を処理中です。
2025-05-01 19:01:23,457 - INFO - task2 を処理中です。
2025-05-01 19:01:23,458 - INFO - task3 を処理中です。

この場合、None"" に対するログが存在しない。

e.g.2) 改善例:continue 前にログ記録を挿入する

# 【1】logging モジュールを読み込む
import logging

# 【2】ログ出力設定(INFO以上)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】処理対象リストを定義
data = ["task1", None, "task2", "", "task3"]

# 【4】ループでリストを順に処理
for item in data:
    # 【5】無効なデータを検出したらログを出してスキップ
    if not item:
        logging.warning("無効なデータ(Noneまたは空文字)を検出したためスキップします。")
        continue  # 【6】次のループへ進む

    # 【7】有効なデータに対して処理とログ出力
    logging.info(f"{item} を処理中です。")
実行結果
2025-05-01 19:02:45,123 - INFO - task1 を処理中です。
2025-05-01 19:02:45,124 - WARNING - 無効なデータ(Noneまたは空文字)を検出したためスキップします。
2025-05-05 19:02:45,125 - INFO - task2 を処理中です。
2025-05-05 19:02:45,126 - WARNING - 無効なデータ(Noneまたは空文字)を検出したためスキップします。
2025-05-05 19:02:45,127 - INFO - task3 を処理中です。

また、条件ごとの処理を関数に分離することで、より構造化された設計も可能である。これは解答例にて併せて示す。

解答例とコードによる実践

# 【1】ログ出力用モジュールを読み込む
import logging

# 【2】ログ出力の設定(ログレベルと表示形式)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】処理対象データのリストを定義する(Noneや空文字列はスキップ対象)
data = ["task1", None, "task2", "", "task3", " ", "task4"]

# 【4】処理済み件数とスキップ件数の初期化
processed_count = 0
skipped_count = 0

# 【5】ループを開始して各データを処理
for item in data:
    # 【6】itemがNoneまたは空に類する場合はスキップ
    if not item or item.strip() == "":
        # 【7】スキップ理由をログに記録
        logging.warning(f"スキップ対象データを検出(値: {item!r}")
        # 【8】スキップ件数を加算
        skipped_count += 1
        # 【9】次のループに進む
        continue

    # 【10】有効なデータの処理をログに記録
    logging.info(f"{item} を処理中です。")
    # 【11】処理済み件数を加算
    processed_count += 1

# 【12】ループ終了後に集計結果を表示
print("\n=== 処理統計 ===")
print(f"処理済み件数: {processed_count}")
print(f"スキップ件数: {skipped_count}")

# 【13】集計結果もログに記録(任意)
logging.info(f"処理済み件数: {processed_count}, スキップ件数: {skipped_count}")
実行結果(標準出力)
=== 処理統計 ===
処理済み件数: 4
スキップ件数: 3
実行結果(ログ出力例)
2025-05-01 19:15:01,123 - INFO - task1 を処理中です。
2025-05-01 19:15:01,124 - WARNING - スキップ対象データを検出(値: None)
2025-05-01 19:15:01,125 - INFO - task2 を処理中です。
2025-05-01 19:15:01,126 - WARNING - スキップ対象データを検出(値: '')
2025-05-01 19:15:01,127 - INFO - task3 を処理中です。
2025-05-01 19:15:01,128 - WARNING - スキップ対象データを検出(値: ' ')
2025-05-01 19:15:01,129 - INFO - task4 を処理中です。
2025-05-01 19:15:01,130 - INFO - 処理済み件数: 4, スキップ件数: 3

上記における設計のポイントは以下

  1. 副作用を最小限に制御
    continue を使用しても、その直前にスキップ対象のログを記録することで副作用を解消できる。
  2. 構造的に管理することで信頼性が向上
    処理件数とスキップ件数を明示的にカウントすることで、処理の透明性が高まり、テストやレビューがしやすくなる。
  3. Pythonicな実装習慣
    ログ記録は状態変化の前に書くべきである。
    continue は早期脱出の手段であり、意味づけを明確にすることが望ましい。

また、条件ごとの処理を関数に分離することで、より構造化された設計も可能である。

# 【1】ログ出力モジュールを読み込む
import logging

# 【2】ログ出力の設定(ログレベルとフォーマット)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】スキップ判定関数:None や 空文字、空白文字列を除外
def is_valid(item):
    # 【4】itemがNoneまたは空白のみの文字列ならFalse
    return bool(item and item.strip())

# 【5】有効データの処理(ログ出力とカウント)
def process_item(item):
    logging.info(f"{item} を処理中です。")

# 【6】スキップ対象の処理(ログ出力とカウント)
def skip_item(item):
    logging.warning(f"スキップ対象データを検出(値: {item!r}")

# 【7】メイン処理関数
def main():
    # 【8】処理対象データのリスト
    data = ["task1", None, "task2", "", "task3", " ", "task4"]

    # 【9】カウント初期化
    processed_count = 0
    skipped_count = 0

    # 【10】データを順に処理
    for item in data:
        if not is_valid(item):
            skip_item(item)
            skipped_count += 1
            continue

        process_item(item)
        processed_count += 1

    # 【11】統計情報の出力
    print("\n=== 処理統計 ===")
    print(f"処理済み件数: {processed_count}")
    print(f"スキップ件数: {skipped_count}")

    # 【12】統計をログにも記録
    logging.info(f"処理済み件数: {processed_count}, スキップ件数: {skipped_count}")

# 【13】スクリプトのエントリーポイント
if __name__ == "__main__":
    main()

改良点と構造化の意義を挙げる。

改良内容 解説
is_valid() 判定ロジックを1か所にまとめて変更しやすくし、可読性と再利用性を向上させた構造である。
process_item() 実行処理を1行で抽象化し、将来的に処理内容を拡張しやすくした構造である。
skip_item() スキップ処理を明示的に切り出し、スキップ理由をログ出力として明確に記録できるようにした構造である。
main() 全体の処理を関数にまとめ、スクリプト構成のエントリーポイントを明示した構造である。
実行結果(標準出力)
=== 処理統計 ===
処理済み件数: 4
スキップ件数: 3
実行結果(ログ出力例)
2025-05-01 19:15:01,123 - INFO - task1 を処理中です。
2025-05-01 19:15:01,124 - WARNING - スキップ対象データを検出(値: None)
2025-05-01 19:15:01,125 - INFO - task2 を処理中です。
2025-05-01 19:15:01,126 - WARNING - スキップ対象データを検出(値: '')
2025-05-01 19:15:01,127 - INFO - task3 を処理中です。
2025-05-01 19:15:01,128 - WARNING - スキップ対象データを検出(値: ' ')
2025-05-01 19:15:01,129 - INFO - task4 を処理中です。
2025-05-01 19:15:01,130 - INFO - 処理済み件数: 4, スキップ件数: 3

Q.12-7

時系列リストを走査し、急激な変化(前後の差が閾値以上)を持つデータ点を continue により除外し、滑らかな系列を構築せよ。

問題背景

【1】時系列データとノイズ除去の必要性

実際のデータ分析や信号処理では、センサーデータや市場データなどの時系列系列に突発的な異常値(スパイク)が含まれることがある。これらの値は、解析や平均化処理を歪める要因になるため、前後の値との急激な差分(デルタ)によって除外することが多い。

【2】continue を使ったスキップ処理

continue 文はループ中で条件を満たした場合に、そのデータ点をスキップして次のイテレーションへ進む構文である。異常と判断したデータ点に対して continue を用いることで、一連の滑らかな時系列列を再構成することが可能である。

【3】 Pythonicな処理構造

処理済みリスト(滑らかな系列)を新たに構築し、そこに許容されたデータ点のみを追加する。前後の変化量を評価し、絶対値差が閾値以上の場合にスキップすることで、突発的変化を除外する。
可読性と保守性を意識し、条件分岐 → continueappend の構造を明確化する。

解答例とコードによる実践

# 【1】ログ出力のためにloggingを読み込む
import logging

# 【2】ログ出力の基本設定(INFOレベル)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】元の時系列データ(例:センサーデータ)
raw_series = [100, 102, 105, 180, 108, 110, 250, 112, 115]

# 【4】変化量の閾値(これ以上の変化があれば異常とみなす)
threshold = 30

# 【5】滑らかな時系列を格納するリストを初期化
smoothed_series = []

# 【6】インデックス1から末尾-1まで走査(両端の前後比較ができないため除外)
for i in range(1, len(raw_series) - 1):
    prev = raw_series[i - 1]  # 【7】1つ前のデータ点
    curr = raw_series[i]      # 【8】現在のデータ点
    nxt  = raw_series[i + 1]  # 【9】1つ後のデータ点

    # 【10】前後の差分を計算
    delta_prev = abs(curr - prev)
    delta_next = abs(curr - nxt)

    # 【11】もしどちらかの変化が閾値以上なら異常値とみなし除外
    if delta_prev >= threshold or delta_next >= threshold:
        logging.warning(f"急激な変化を検出:index={i}, 値={curr}, Δprev={delta_prev}, Δnext={delta_next}")
        continue  # 【12】この点を除外して次のデータへ進む

    # 【13】滑らかなデータとして追加
    smoothed_series.append(curr)
    logging.info(f"データ追加:index={i}, 値={curr}")

# 【14】処理結果を表示
print("滑らかな時系列データ:", smoothed_series)
実行結果(標準出力:)
滑らかな時系列データ: [102, 105, 108, 110, 112]
実行結果(ログ出力例:)
2025-05-01 19:20:01,123 - INFO - データ追加:index=1, 値=102
2025-05-01 19:20:01,124 - INFO - データ追加:index=2, 値=105
2025-05-01 19:20:01,125 - WARNING - 急激な変化を検出:index=3, 値=180, Δprev=75, Δnext=72
2025-05-01 19:20:01,126 - INFO - データ追加:index=4, 値=108
2025-05-01 19:20:01,127 - INFO - データ追加:index=5, 値=110
2025-05-01 19:20:01,128 - WARNING - 急激な変化を検出:index=6, 値=250, Δprev=140, Δnext=138
2025-05-01 19:20:01,129 - INFO - データ追加:index=7, 値=112

本問のポイントは以下

  1. スキップのロジック構造
    前後の差分 (Δ) を利用し、異常点かどうかを判定している。continue はログ出力の後に記述されており、スキップ対象の理由が確実に記録される構成である。
  2. なぜ両端は除外するのか
    データの前後を比較する処理では、最初と最後の点(i=0, i=n-1)は比較対象が一方向しか存在せず、前後差が定義できないためである。
  3. Pythonicな観点
    条件の複雑さを避け、明確な早期脱出(continue)を使用することでコードの流れをシンプルに保っている。ログレベルを INFOWARNING に分けることで、データの区別がしやすく、運用ログとしても有効である。

Q.12-8

.startswith() によって特定の文字列で始まる行を continue で除外し、それ以外の処理にのみ .append() を実行するフィルタ構造を設計せよ。

問題背景

【1】.startswith() の役割と使用法

.startswith() は、文字列が特定の接頭辞(prefix)で始まっているかを判定するためのメソッドである。
引数として文字列または文字列のタプルを受け取り、True / False を返すブール関数である。

"INFO: process started".startswith("INFO")  # → True
"ERROR: failed".startswith("INFO")         # → False

【2】continue による行のスキップ

continue は、ループ内で特定の条件を満たす行をスキップして次のイテレーションへ移るために使う。フィルタリング目的で使用することで、特定条件の行を処理対象から除外することができる。

【3】.append() によるリスト構築

.append() は、リストに要素を逐次追加するための標準メソッドであり、フィルタ済みのデータを別リストとして再構築する用途に適している。処理対象外の行を continue により除外した上で、必要な行だけを .append() に通すことで、条件付きリスト構築が実現できる。

解答例とコードによる実践

# 【1】ログモジュールを読み込み、設定を行う
import logging

# 【2】ログの出力設定(INFOレベル以上を表示)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】対象となる文字列リスト(ログのようなデータ行)
lines = [
    "INFO: system ready",
    "# this is a comment",
    "ERROR: disk full",
    "",
    "WARNING: low memory",
    "# temporary line",
    "INFO: user login",
    "CRITICAL: overheating"
]

# 【4】フィルタされた出力行を格納するリストを初期化
filtered_lines = []

# 【5】各行に対して順に処理を行う
for line in lines:
    # 【6】先頭が "#" または空文字列の行を除外対象とする
    if line.startswith("#") or line.strip() == "":
        # 【7】除外理由をログに記録
        logging.warning(f"行をスキップ:'{line}'(コメントまたは空行)")
        continue  # 【8】この行をスキップして次に進む

    # 【9】残りの有効な行をリストに追加
    filtered_lines.append(line)
    # 【10】追加した内容をログ出力
    logging.info(f"行を追加:'{line}'")

# 【11】最終的なフィルタ済みリストの表示
print("\n--- フィルタ後の出力 ---")
for entry in filtered_lines:
    print(entry)
実行結果(標準出力:)
--- フィルタ後の出力 ---
INFO: system ready
ERROR: disk full
WARNING: low memory
INFO: user login
CRITICAL: overheating
実行結果(ログ出力例:)
2025-05-01 19:30:01,123 - INFO - 行を追加:'INFO: system ready'
2025-05-01 19:30:01,124 - WARNING - 行をスキップ:'# this is a comment'(コメントまたは空行)
2025-05-01 19:30:01,125 - INFO - 行を追加:'ERROR: disk full'
2025-05-01 19:30:01,126 - WARNING - 行をスキップ:''(コメントまたは空行)
...

上記の設計のポイントは以下。

  1. startswith()strip() の併用
    .startswith("#") によってコメント行を、.strip() == "" によって空行を検出して除外している。空白文字だけの行も正しく除外できるように .strip() を併用しているのがポイントである。

  2. continue の位置に注意
    continue はログ出力の後に記述しており、除外の理由をログに記録した上でスキップ処理が完了する設計となっている。

  3. .append() の適用箇所
    .append()continue の後に書かれていないため、除外対象の行は .append() に到達しない。これにより、フィルタされたリストが有効な行のみを保持するように構成されている。

本問のポイントは以下。
.startswith() を使った文字列フィルタリングは、ログ行や設定ファイルのパースにおいて極めて有効である。
continue によるスキップ処理と .append() による追加処理を明確に分離し、構造的に配置することで、読みやすく副作用のないコードを実現できる。
・ このような設計は、設定ファイルパーサ、ログ収集処理、CSVクリーナーなどの構築時に活用される基本的な手法である。

あとがき

今回は「ループ処理における breakcontinue」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「例外処理」について扱う予定です。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?