コレクション
7-1

自前ループより標準のコレクション処理

存在チェックや集計のたびに for とフラグ変数で自前ループを書くと、「何をしたいのか」がループの構造に埋もれてしまう。同じ走査ロジックが複数箇所に重複すれば、仕様変更時の修正漏れも起きる。any() / sum() / 内包表記など、言語が備えるコレクション処理に置き換えれば意図が1行で伝わる。

01 延滞している請求書があるかの判定

Bad

フラグ変数を用意して for で回し、見つかったら break。定型の探索処理を毎回手書きしている

フラグ変数+自前ループ
def has_overdue_invoice(invoices: list[Invoice], today: date) -> bool:
    found = False
    for invoice in invoices:
        if invoice.is_paid:
            continue
        if invoice.due_date < today:
            found = True
            break
    return found
Good

any() とジェネレータ式なら「延滞中の未払いが1件でもあるか」という問いがそのままコードになる

any() で宣言的に
def has_overdue_invoice(invoices: list[Invoice], today: date) -> bool:
    return any(
        not invoice.is_paid and invoice.due_date < today
        for invoice in invoices
    )

Python では any / all / sum / max / min とジェネレータ式の組み合わせが、Java の Stream API に相当する基本装備。フラグ変数と break が消えるだけでなく、途中で見つかれば走査が打ち切られる点も自前ループと同じ。

02 経費明細の集計と抽出

Bad

1つのループに合計の加算と上限超過の抽出が同居し、どちらの仕様変更でも同じループを触ることになる

複数の目的が絡んだループ
def summarize_expenses(expenses: list[Expense]) -> tuple[int, list[str]]:
    total = 0
    over_limit_titles = []
    for expense in expenses:
        total += expense.amount
        if expense.amount > 30_000:
            over_limit_titles.append(expense.title)
    return total, over_limit_titles
Good

合計は sum、抽出は内包表記。目的ごとに独立した1行になり、片方の変更がもう片方に波及しない

目的別に sum と内包表記へ分解
def total_amount(expenses: list[Expense]) -> int:
    return sum(expense.amount for expense in expenses)

def over_limit_titles(expenses: list[Expense]) -> list[str]:
    return [
        expense.title
        for expense in expenses
        if expense.amount > 30_000
    ]

「ループを1回で済ませたい」と複数の目的を詰め込むより、目的ごとに分けた方が読みやすく変更にも強い。明細が数千件程度なら走査回数の差は誤差で、可読性の利得の方がずっと大きい。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第7章。コード例は原則を自分の題材で表現し直したオリジナル。
7-1

自前ループより標準のコレクション処理

存在チェックや集計のたびに for とフラグ変数で自前ループを書くと、「何をしたいのか」がループの構造に埋もれてしまう。同じ走査ロジックが複数箇所に重複すれば、仕様変更時の修正漏れも起きる。any() / sum() / 内包表記など、言語が備えるコレクション処理に置き換えれば意図が1行で伝わる。

01 延滞している請求書があるかの判定

Bad

フラグ変数を用意して for で回し、見つかったら break。定型の探索処理を毎回手書きしている

フラグ変数+自前ループ
def has_overdue_invoice(invoices: list[Invoice], today: date) -> bool:
    found = False
    for invoice in invoices:
        if invoice.is_paid:
            continue
        if invoice.due_date < today:
            found = True
            break
    return found
Good

any() とジェネレータ式なら「延滞中の未払いが1件でもあるか」という問いがそのままコードになる

any() で宣言的に
def has_overdue_invoice(invoices: list[Invoice], today: date) -> bool:
    return any(
        not invoice.is_paid and invoice.due_date < today
        for invoice in invoices
    )

Python では any / all / sum / max / min とジェネレータ式の組み合わせが、Java の Stream API に相当する基本装備。フラグ変数と break が消えるだけでなく、途中で見つかれば走査が打ち切られる点も自前ループと同じ。

02 経費明細の集計と抽出

Bad

1つのループに合計の加算と上限超過の抽出が同居し、どちらの仕様変更でも同じループを触ることになる

複数の目的が絡んだループ
def summarize_expenses(expenses: list[Expense]) -> tuple[int, list[str]]:
    total = 0
    over_limit_titles = []
    for expense in expenses:
        total += expense.amount
        if expense.amount > 30_000:
            over_limit_titles.append(expense.title)
    return total, over_limit_titles
Good

合計は sum、抽出は内包表記。目的ごとに独立した1行になり、片方の変更がもう片方に波及しない

目的別に sum と内包表記へ分解
def total_amount(expenses: list[Expense]) -> int:
    return sum(expense.amount for expense in expenses)

def over_limit_titles(expenses: list[Expense]) -> list[str]:
    return [
        expense.title
        for expense in expenses
        if expense.amount > 30_000
    ]

「ループを1回で済ませたい」と複数の目的を詰め込むより、目的ごとに分けた方が読みやすく変更にも強い。明細が数千件程度なら走査回数の差は誤差で、可読性の利得の方がずっと大きい。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第7章。コード例は原則を自分の題材で表現し直したオリジナル。