Python基礎

『読める・速い・止まる』Pythonの制御構文|if / for / whileの設計術

if / for / while、実務ではどれから直せば読みやすくなる?

分岐がぐちゃぐちゃ、ループが遅い、while True が止まらない…その悩み、今日で終わらせましょう。

この記事は、pythonbunseki.comの定番トーンで「制御構文の設計」を実務目線で解説します。ポイントは、(1) 条件は名詞化して早期return、(2) ループはデータの“形”から設計enumerate/zip/内包表記)、(3) 終了条件は先に置くbreak/continue/returnの“意図の強さ”で使い分け)。

この記事で身に付くこと

  • 読める条件分岐:条件を名詞化して早期returnに落とす型
  • 速いループ設計:イテラブルに合わせたfor・内包表記の使い分け
  • 止まる処理whileで終了条件を先に置く・ガード節の付け方

よくあるつまずきは「読めない・遅い・止まらない」

レビューで最も指摘が入るのはこの3つ。いずれも“設計”で解消できます。

  • 条件が読めないif a and not b or c == 3 のように意図が見えない。
  • 反復が遅い:ループ内で連結や検索を繰り返して O(n^2) になる。
  • 終了条件が曖昧while True のまま無限ループ、breakが乱立。

解決の順番は「条件に名前」→「データの形で回す」→「終了条件は先に置く」。この型に沿って整理していきます。

現場メモ(ふみと)

データ/マーケティングサイエンティストとして10年、レビューで「読める」と感じるコードは決まって、(1) 条件が名詞is_valid_userなど)、(2) ループはデータ駆動for user in usersのように名前が語る)、(3) 早期returnでネストが浅い。この3点でした。

if:条件は名詞化&早期returnで浅くする

複雑な式をそのままifに書かないのがコツ。意図を「名前」に切り出してから判定しましょう。

悪い例

def discount(price, member, coupon):
    if price > 10000 and (member == "gold" or coupon == "VIP") and not (price % 2 == 1):
        return int(price * 0.9)
    else:
        return price

良い例(意図を名前に)

def discount(price, member, coupon):
    is_large = price > 10000
    is_special = member == "gold" or coupon == "VIP"
    is_even_price = price % 2 == 0
    if not (is_large and is_special and is_even_price):
        return price  # 早期returnでネストを浅く
    return int(price * 0.9)
  • 短絡評価A and B は AがFalseならBを評価しない。副作用のある式は入れない。
  • 連鎖比較0 < x < 10 は読みやすい。
  • 空の真偽値[]/""/0/None は False 相当。if not items: が定番。

for:イテラブルの「形」から設計する

手元のデータ構造に合わせて書けば、意味が伝わるコードになります。

users = [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]
for user in users:  # 変数名は意味が伝わるように
    print(user["name"])
for i, user in enumerate(users, start=1):
    print(i, user["name"])
names = ["A", "B", "C"]
ages = [23, 31, 27]
for name, age in zip(names, ages):
    print(name, age)
for key, value in mapping.items():
    ...

内包表記は「式1つ・副作用なし」の時だけが基本です。

# 良い:式が1つで副作用なし
squares = [x * x for x in range(10) if x % 2 == 0]

計算量の目安(ざっくり)

  • ネスト1段:O(n)、2段:O(n^2)
  • まず先に絞る(条件でfilter)→必要な要素だけ回す。

while:終了条件は先に決める(可能ならfor優先)

外部要因で終わる処理(入出力・逐次取得など)だけ while を選び、ガード節(最大回数・タイムアウト)を用意します。

def read_until_empty(lines):
    result = []
    for line in lines:  # 可能ならforでイテラブルを回す
        if not line.strip():
            break
        result.append(line)
    return result
while True:
    line = input(": ")
    if not line:
        break  # 明示的に終了
    process(line)

for-else / while-else:breakしなかった時の後処理

if-elseelseとは意味が違います。ループをbreakせず正常終了した時だけ実行されます。

# 例:素数判定
def is_prime(n: int) -> bool:
    if n < 2:
        return False
    for p in range(2, int(n ** 0.5) + 1):
        if n % p == 0:
            return False  # 見つかったら即終了
    else:
        return True

break / continue / return の意図の強さ

  • return:関数全体を終了(最も強い)
  • break:外側の1ループを終了
  • continue:今回の1回だけスキップ

多用して読みにくければ、条件を名詞化して早期returnに寄せましょう。

テンプレ集:集計・検索・変換

A) 集計(group)

from collections import Counter, defaultdict

# 1. 件数カウント

c = Counter(event\["type"] for event in events)

# 2. 合計/平均(辞書で蓄積)

sums, counts = defaultdict(int), defaultdict(int)
for e in events:
k = e\["type"]
sums\[k] += e\["value"]
counts\[k] += 1
avg = {k: sums\[k] / counts\[k] for k in sums}

B) 検索(find)

# 条件を関数に名詞化
def is_target(u):
    return u["active"] and u["score"] >= 80

# 1. 最初の1件

found = None
for u in users:
if is\_target(u):
found = u
break

# 2. なければデフォルト

result = found or {"id": -1, "name": "N/A"}

C) 変換(map / filter)

# 1. シンプル → 内包表記
names = [u["name"].strip().title() for u in users if u["active"]]
# 2. 複雑 → for文に戻す
cleaned = []
for u in users:
    if not u["active"]:
        continue
    name = u["name"].strip().title()
    cleaned.append({**u, "name": name})

用途別ミニレシピ(最小構成)

  • データ整形for x in rowsif 条件で先に絞る → 副作用がなければ内包表記。
  • 検索/検知:見つからない時の処理はfor-else or 早期return。
  • 対話/監視while True+ガード節(最大回数/タイムアウト)。
  • 業務スクリプト:条件は名詞化 → 早期return → ログ出力。→ [内部リンク:例外処理とログ設計]

ハンズオン(15分×6問)

Q1:偶数だけ2乗(内包表記)

nums = [1,2,3,4,5,6]
# TODO: 偶数だけ2乗したリストを作る
[x*x for x in nums if x % 2 == 0]

Q2:最初の80点以上のユーザ(for-else)

users = [{"name":"A","score":70},{"name":"B","score":85}]
# TODO: 80点以上が見つかったらname、なければ"N/A"
for u in users:
    if u["score"] >= 80:
        res = u["name"]
        break
else:
    res = "N/A"

Q3:入力が空なら終了(while)

# TODO: 入力を受け取り、空行で終了して件数を表示
cnt = 0
while True:
    line = input(": ")
    if not line:
        break
    cnt += 1
print(cnt)

Q4:ネストを浅く(早期return)

def is_valid(age, email):
    # TODO: 年齢が18以上、メールに"@"を含む
def is_valid(age, email):
    if age < 18:
        return False
    if "@" not in email:
        return False
    return True

Q5:enumerateでインデックス表示

items = ["a","b","c"]
# TODO: 1始まりで "1:a" の形で出力
for i, x in enumerate(items, start=1):
    print(f"{i}:{x}")

Q6:条件名詞化の練習

user = {"age":25, "role":"admin", "active":True}
is_admin = user["role"] == "admin"
is_active = user["active"]
if is_admin and is_active:
    ...

アンチパターン → 一発で直す

1) フラグだらけの while True

# NG
while True:
    x = get()
    if x is None:
        break
    if x == "ERR":
        handle()
        continue
    if len(x) > 100:
        process(x)
# → OK(ガード節+関数化)
def should_process(x):
    return x and x != "ERR" and len(x) > 100

for x in iter(get, None):  # get() が None を返すまで
if not should\_process(x):
continue
process(x)

2) ループ内での逐次 + 連結

# NG(O(n^2))
s = ""
for x in items:
    s += x
# → OK(O(n))
s = "".join(items)

3) range(len(seq)) で回す

# NG
for i in range(len(seq)):
    print(seq[i])
# → OK
for x in seq:
    print(x)

4) 多段ネスト

# NG
for a in A:
    for b in B:
        if cond(a, b):
            do(a, b)
# → OK(早期continue / 先に絞る)
for a in A:
    if not cond_a(a):
        continue
    for b in (x for x in B if cond_b(x)):
        if cond(a, b):
            do(a, b)

ちょい上級:代入式(ウォルラス :=)の安全な使い方

「同じ計算を2回書かない」ための省略です。読みづらくなるなら使わない判断も正解。

import re

# パースに成功した行だけ処理

while (line := input(": ")):
if (m := re.match(r"^(\d+),(\w+)$", line)):
num, name = int(m.group(1)), m.group(2)
handle(num, name)

テストしやすい制御構文(pytest)

分岐・ループを純粋関数に寄せるとテストが楽になります。最小例:

def pick_actives(users):
    return [u for u in users if u.get("active")]

def test\_pick\_actives():
users = \[{"active": True}, {"active": False}]
assert pick\_actives(users) == \[{"active": True}]

→ [内部リンク:単体テストpytest入門]

ここまでのまとめ

  • 条件は名詞化して早期return。
  • 反復はイテラブル設計enumerate/zip/内包表記)。
  • 終了条件は先置き、ガード節を忘れない。

今日やること(30〜45分)

  • 条件の名詞化:既存スクリプトのifを1カ所、名前付きブールに置換。
  • 内包表記:式1つのループを1本、内包表記に変換。
  • for-else:見つからなかった時の処理をelseへ。
  • pytest:分岐ロジックを1本テスト化。→ [内部リンク:単体テストpytest入門]

関連記事(サイト内)

Python関数
Python関数とスコープの設計術:I/O分離×型ヒントで再利用性とテスト容易性を最大化

関数がどんどん太ってテストしづらい…どう整理すればいい? globalやprintが混ざっていて、ユニットテストを書く気力が出ない… この記事では、関数とスコープの設計を“純度×境界×型”の視点で整え ...

例外処理
Python実務の型:例外処理と構造化ログでエラーに強いコードを書く

例外処理って、結局どこまでやれば“実務で困らない”の? ログも整えるのって大変そう…最低限の型、ください! この記事は、pythonbunseki.comの実務トーンで「防ぐ→気づく→復旧する」をコー ...

Python標準ライブラリ
Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート

外部ライブラリを増やさずに、コードの品質と保守性をグッと上げたい…。 標準ライブラリ“だけ”で、どこまで実務が回せる? 今回はそんな悩みを解決するために、Python標準ライブラリの“珠玉の10選”を ...

テスト
【コピペOK】pytestで“壊れないPython”を作る12ステップ

「昨日は動いてたのに、今日は壊れた…」 データ分析やETL、機械学習のコードで多発するこの悲劇。実は“テスト不在”が9割です。 本記事は、pytestで“壊れないPython”を作るための実務ガイド。 ...

TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

TechAcademy 無料相談

株式会社キカガク AI人材長期育成コース(受講料:237,600円~)

キカガク 無料相談

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎