【Python】関数設計の極意:処理の「意味」でまとめる「関心の分離」

プログラミングにおいて、関数はコードを整理し、再利用性を高めるための基本的な単位です。しかし、ただ単に繰り返し現れるコードをまとめるだけでは、真に読みやすく、変更に強いコードにはなりません。重要なのは、その関数が**「何を」するのか**だけでなく、**「なぜ」その処理が行われるのか**という「意味」や「目的」でまとめることです。

これは「関心の分離(Separation of Concerns)」と呼ばれる設計原則に通じる考え方であり、各関数が明確な一つの「関心事」にのみ責任を持つように設計することで、コードの可読性、保守性、テスト容易性が飛躍的に向上します。

目次

処理でまとめた関数の問題点

例えば、次のようなPythonスクリプトを考えてみましょう。このスクリプトは、商品データが記載されたCSVファイルを読み込み、特定の条件(価格が100未満)を満たす商品の名前を出力します。

import csv

# 悪い例:処理のまとまりで関数化しているため、意図が不明瞭

def process_and_print_items(filepath: str):
    """
    指定されたファイルパスから商品を読み込み、
    価格が特定の条件を満たす商品名を出力する。
    """
    with open(filepath, mode="r", encoding="utf-8") as file:
        reader = csv.reader(file)
        next(reader) # ヘッダー行をスキップ
        for row_data in reader:
            # ここに処理の塊がある
            product_name = row_data[0]
            product_price = int(row_data[1])
            
            if product_price < 100: # 特定の条件
                print(product_name)

# 実行
if __name__ == "__main__":
    # 仮のCSVファイルを作成
    with open("sample_products.csv", "w", encoding="utf-8") as f:
        f.write("Name,Price\n")
        f.write("Apple,120\n")
        f.write("Banana,80\n")
        f.write("Orange,95\n")
        f.write("Grape,250\n")

    process_and_print_items("sample_products.csv")

一見すると、process_and_print_items という関数に処理がまとまっているように見えます。しかし、この関数は以下の複数の「関心事」を抱えています。

  1. ファイルからCSVを読み込む
  2. 各行のデータを解析する(文字列から数値への変換など)
  3. 特定の条件で価格を判定する
  4. 結果を出力する

このように複数の責務を持つ関数は、次のような問題を引き起こします。

  • 意図が分かりにくい: 関数名だけでは、具体的に「何のために」これらの処理が行われているのかが伝わりにくい。
  • 再利用性が低い: 「CSVからデータを読み込むだけ」「価格を判定するだけ」といった個別の処理を別の場所で使いたい場合、この関数をそのまま再利用できない。
  • 単体テストが困難: ファイル操作、データ解析、条件判定、出力といった異なる要素が混ざっているため、それぞれのロジックを独立してテストするのが難しい。

解決策:意味のまとまりで関数化する

これらの問題を解決するには、各関数が「意味のある一つの関心事」に集中するようにコードを分割します。

1. データの読み込みと解析 (read_products_from_csv)

ファイルから商品データを読み込み、適切な型に変換して返すことだけを担う関数を作成します。Pythonのジェネレーターを使うことで、メモリ効率良くデータを処理できます。

# 良い例①:データの読み込みと解析に特化
def read_products_from_csv(filepath: str):
    """
    CSVファイルから商品名と価格を読み込み、
    (商品名: str, 価格: int)のタプルをジェネレーターとして返す。
    """
    with open(filepath, mode="r", encoding="utf-8") as file:
        reader = csv.reader(file)
        next(reader) # ヘッダーをスキップ
        for row_data in reader:
            product_name = row_data[0]
            product_price = int(row_data[1])
            yield product_name, product_price # ジェネレーターとして返す

この関数は、「CSVから商品を読み込む」という明確な意味のまとまりを持ちます。

2. 条件の判定 (is_promotional_price)

価格がプロモーション対象(例: 100未満)かどうかを判定する関数です。この関数は純粋な「問い合わせ」であり、副作用を持ちません。

# 良い例②:条件判定に特化
def is_promotional_price(price: int) -> bool:
    """指定された価格がプロモーション対象(100未満)かを判定する。"""
    return price < 100

is_promotional_price という名前から、この関数が価格の条件判定を行うことが自明です。

3. メインロジック (display_promotional_items)

最後に、これらの関数を組み合わせて、最終的な目的を達成するメインのロジックを記述します。

# 良い例③:メインロジック (目的達成のための組み合わせ)
def display_promotional_items(filepath: str):
    """
    CSVファイルからプロモーション対象の商品を抽出し、その名前を表示する。
    """
    products = read_products_from_csv(filepath) # 読み込み
    for name, price in products:
        if is_promotional_price(price): # 判定
            print(f"プロモーション対象: {name}") # 出力
            
# 実行例
if __name__ == "__main__":
    # 仮のCSVファイルを作成 (再利用)
    with open("sample_products.csv", "w", encoding="utf-8") as f:
        f.write("Name,Price\n")
        f.write("Apple,120\n")
        f.write("Banana,80\n")
        f.write("Orange,95\n")
        f.write("Grape,250\n")

    display_promotional_items("sample_products.csv")

このように分割することで、

  • read_products_from_csv: ファイルI/Oとデータ解析の責務だけを持つ。
  • is_promotional_price: 純粋な条件判定の責務だけを持つ。
  • display_promotional_items: 全体の流れを制御し、各部品を組み合わせる責務を持つ。

と、各関数が明確な「意味」のまとまりを持つようになりました。


まとめ

関数を分割する際は、単にコード行数を減らすためではなく、**その関数が「どのような目的」を達成するために存在するか**という視点で考えることが重要です。

  • 意味のあるまとまりで関数化する: 各関数に「関心事」を一つだけ持たせる。
  • 処理の意図を明確にする: 関数名で「何を」するだけでなく「なぜ」するのかを推測させる。
  • 再利用性とテスト容易性を高める: 独立した部品として機能させることで、様々な場面での利用や、単体テストが容易になる。

「この関数は何のため、誰のためにあるのか?」という問いを常に持ち、意味のまとまりを意識した関数設計を心がけましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次