Pythonでクラスが必要ない設計とは?関数と辞書だけで読みやすいコードを書く開発の極意

関数と辞書だけで構築するPythonのデータ指向設計の全体概念図 プログラミング言語

クラスを使わなければ、オブジェクト指向プログラミングではない──そんな思い込み、ありませんか?

確かに、JavaやC#ではクラスが第一級の構成要素ですが、Pythonはもっと柔軟です。
実際、私がこれまで携わってきた数多くのプロジェクトで、無理にクラスを導入したためにかえって複雑性が増したケースを何度も見てきました。
では、クラスを排除したらどうなるのか。
答えはシンプルです。
データは辞書で持ち、振る舞いは関数で書く。
これだけです。

このアプローチの最大の利点は、状態と処理が明確に分離される点にあります。
クラスでは、メソッドがインスタンス変数を暗黙的に参照するため、副作用が発生しやすく、テストの際にモックが必要になることも少なくありません。
一方、関数と辞書の組み合わせでは、入力が明示的で、出力も予測可能。
純粋関数を積み重ねることで、コードの理解やデバッグが格段に容易になります。

例えば、ユーザーデータを処理する場合を考えてみましょう。

def validate_user(data):
    return "name" in data and data["age"] > 0

def format_user(data):
    return f"{data['name']} ({data['age']}歳)"

このように、辞書を引数に取る関数を並べるだけで、クラスの継承やオーバーライドといった重厚な仕組みが一切不要です。
しかも、これらの関数は単独でテストでき、他のデータ構造にも再利用しやすい。

では、クラスと比較してどう優れているのか。
以下の表にまとめました。

評価軸 クラス指向 関数+辞書指向
コードの冗長性 高い(__init__やselfが必須) 低い(必要な引数だけ)
テストの容易さ インスタンス生成が必要でモックが煩雑になりがち 関数に与える入力と期待出力を確認するだけ
拡張性 継承やミックスインで柔軟だが階層が深くなると保守が大変 新しい関数を追加するだけで、既存コードへの影響が局所的
可読性 状態がどこで変更されるか追いにくい データの変換フローが関数の呼び出し連鎖として明示的

この比較からも分かる通り、関数と辞書だけでも、十分に拡張性があり、かつ読みやすいコードが実現できるのです。
もちろん、大規模なフレームワークや複雑なポリモーフィズムが必要なケースではクラスが有効な場面もあります。
しかし、多くのビジネスロジックやデータ変換処理では、このシンプルな設計が圧倒的に生産性を高めます。

では、具体的にどのような設計パターンがあり、どのようにプロジェクト全体を構築していくのか。
本記事では、実践的なコーディング規約から、辞書の構造を型ヒントで補強するテクニック、さらには関数合成によるパイプライン処理までを段階的に解説します。
クラスに縛られず、Pythonの動的型付けの利点を最大限に活かす方法を、一緒に考えていきましょう。

  1. クラスに頼りすぎるPython開発が抱える過剰設計の問題点
    1. 継承階層の深掘りが招くコードリーディングの困難
    2. インスタンス変数の暗黙的変更がデバッグを難しくする理由
  2. 関数と辞書を主軸に据えたデータ指向設計の基本原則
    1. 状態と振る舞いを分離して副作用を局所化する
    2. データの構造を辞書で明示し、処理を関数で切り出す
  3. クラスレス実装の具体例:ユーザー管理機能をゼロから構築
    1. バリデーション関数で入力データをクリーンにする
    2. 変換関数と保存関数を分離して責務を明確化
    3. エラー情報も辞書で返す統一的なハンドリング
  4. 単体テストが劇的に楽になる:モック不要の純粋関数検証
    1. 入出力だけを見ればいいのでテストコードが短縮される
    2. 辞書フィクスチャを再利用して網羅性を高める
  5. 型ヒントとTypedDictで辞書構造を静的にチェックする方法
    1. TypedDictで必須キーとオプションキーを定義
    2. mypyを組み込んでCIでスキーマ違反を検出
  6. 関数合成とパイプラインで実現する読みやすいデータフロー
    1. 自作パイプ関数で左から右への自然な連鎖
    2. functools.reduceで動的な処理チェーンを構築
  7. あえてクラスを選ぶべきケース:ポリモーフィズムやフレームワーク
    1. GUI部品やプラグイン機構では継承が有効
    2. 大規模OSSフレームワーク内部ではクラスが標準
  8. プロジェクト構成の工夫:ディレクトリ分割と命名規約の実践
    1. schemas.pyで辞書構造を一元管理
    2. operations/配下に関数を機能別に配置
    3. main.pyではパイプラインを組み立てるだけ
  9. まとめ:クラスは最終手段、関数と辞書で十分な理由

クラスに頼りすぎるPython開発が抱える過剰設計の問題点

複雑に絡み合ったクラス継承図とデバッグが困難な様子を示すイラスト

Pythonはマルチパラダイム言語であるにもかかわらず、多くの開発者がJavaやC#からの習慣で、何にでもクラスを導入してしまう傾向にあります。
しかし、この習慣がときに過剰設計を招き、コードの複雑性を著しく高める要因となることは、経験的に明らかです。
特に、ビジネスロジックが主体のアプリケーションでは、クラス階層がもたらす間接性が、生産性や保守性を損なうケースが少なくありません。

では、具体的にどのような問題が生じるのか。
継承の深掘りとインスタンス変数の隠れた変更という、二つの観点から掘り下げます。

継承階層の深掘りが招くコードリーディングの困難

継承は確かに強力な再利用機構ですが、階層が3階層を超えたあたりから、コードの読み手は大きな負担を強いられます。
例えば、BaseProcessorを継承したDataProcessor、さらにそれを継承したUserDataProcessor、そして特別なケース用にSpecialUserDataProcessorといった具合です。
このような構造では、あるメソッドの動作を理解するために、基底クラスの実装を何度も遡らなければなりません。

具体的な弊害を列挙してみましょう。

  • メソッドのオーバーライドが重なると、実際に呼び出される実装がどのクラスにあるのかを特定するのに、IDEの支援があっても時間を要する
  • 基底クラスの変更が、予期しない子クラスの挙動に影響を与える「フラジールベースクラス問題」が発生しやすくなる
  • 抽象メソッドと具象メソッドの区別が曖昧になり、インターフェースの意図を読み解くのに余計な認知負荷がかかる

さらに、Pythonは動的型付けであるため、継承関係が型ヒントに明示されていない場合、コードベース全体を検索しないと全体像が把握できません。
このような状況では、バグ修正や機能追加の際に、影響範囲を正確に見積もることが難しくなります。
結果として、開発者は変更を恐れてコードを避けるようになり、技術的負債が雪だるま式に増大していくのです。

インスタンス変数の暗黙的変更がデバッグを難しくする理由

クラス設計におけるもう一つの落とし穴は、インスタンス変数が多数のメソッドによって暗黙的に変更される点です。
self.statusやself.cacheといった変数が、初期化メソッドだけでなく、バリデーション、変換、出力処理など、あらゆる場所で書き換えられるケースを考えてみてください。

この状態では、ある時点での変数の値を特定することが、まるでグローバル変数の追跡のように困難になります。
なぜなら、その変数が最後に変更されたのがどのメソッドなのかを突き止めるには、呼び出し履歴全体をトレースしなければならないからです。
特に例外が発生した場合、スタックトレースにはメソッド名は表示されても、変数の変更履歴は残りません。
そのため、再現が難しいバグの原因を特定するには、ログ出力を追加して何度も実行を繰り返すしかない──そんな経験をお持ちの方は少なくないでしょう。

この問題を明確にするために、クラスベースと関数ベースのデバッグ容易性を比較してみます。

比較項目 クラス(インスタンス変数あり) 関数+辞書(明示的引数)
状態の変更箇所の特定 メソッド呼び出しを全トレースする必要あり 関数の引数と戻り値だけをチェックすればよい
再現テストの作成 インスタンス生成順序や初期状態の再現が面倒 対象関数に辞書を渡すだけで再現可能
デバッガでのステップ実行 予期せぬメソッドが変数を書き換えるため、意図しない分岐に迷い込む 各関数の入出力が明確なので、ステップ数が少ない

この表からも分かる通り、インスタンス変数に頼るほど、デバッグは推測ゲームと化します。
特に、複数スレッドや非同期処理が絡むと、変数の競合状態まで考慮する必要が生じ、もはや手に負えなくなります。
PythonのGILがあるとはいえ、非同期I/Oとインスタンス変数の組み合わせは、予測不能なバグの温床となるのです。

では、なぜ多くの開発者がこのような設計を選んでしまうのか。
それは、クラスが「現実世界のオブジェクト」を自然に表現できるという誤解と、フレームワークや書籍がクラスベースの例を多く提示しているからに過ぎません。
しかし、本質的なデータ処理においては、状態の変更を局所化し、入出力を明示する方が、はるかに堅牢で読みやすいコードへと導きます。
次のセクションでは、その具体的な代替手段として、関数と辞書を中心に据えた設計の基本原則を提示します。

関数と辞書を主軸に据えたデータ指向設計の基本原則

関数の矢印と辞書のアイコンがシンプルに連鎖するデータ指向設計の模式図

クラスベースの設計が持つ問題点を踏まえた上で、ここではその対案として、関数と辞書を中心に据えたデータ指向設計の核心を解説します。
このアプローチは、データとロジックを明確に分離することで、コードの予測可能性とテスト容易性を飛躍的に高めることを目的としています。
決して「クラスを否定する」のではなく、「クラスが不要な場面では使わない」という、取捨選択の意識が重要です。

状態と振る舞いを分離して副作用を局所化する

オブジェクト指向の教科書では、状態と振る舞いを一体にすることが美徳とされます。
しかし、実務においてはこの一体性がかえって混乱を生む原因になります。
なぜなら、状態が複数のメソッドを経由して変更されると、その変更履歴が暗黙的になり、結果としてシステム全体の複雑性が指数関数的に増加するからです。

データ指向設計では、状態は単なるデータ構造(辞書)として保持し、振る舞いはそのデータを受け取り新たなデータを返す純粋関数として定義します。
この分離によって、以下のメリットが得られます。

  • 副作用が関数の内部に閉じ込められるため、関数の外側に影響を与えるリスクが劇的に低減する
  • 同じ入力に対して常に同じ出力を返すため、再現性が保証され、デバッグが直感的になる
  • 関数がデータの変換のみに専念するので、ロジックがシンプルになり、可読性が向上する

例えば、ユーザー情報を更新する処理を考えます。
クラスではuser.update_email(new_email)のようにメソッドがインスタンス変数を書き換えますが、データ指向ではupdate_email(user_dict, new_email)という関数が新しい辞書を返す形を取ります。
このとき、元の辞書は不変に保たれるため、変更履歴やロールバックも容易になります。

この考え方を徹底するために、関数内での外部変数への代入や、ファイルI/O、ネットワークアクセスといった副作用のある処理は、可能な限り関数の境界でまとめて実行し、それ以外の部分は純粋な変換ロジックに切り分けるのがコツです。
そうすることで、副作用を局所化し、全体の信頼性を高めることができます。

データの構造を辞書で明示し、処理を関数で切り出す

では、具体的にどのようにデータと処理を設計するのか。
まず、データは辞書(dict)という組み込み型を基本とします。
辞書はキーと値のペアで構成され、JSONとの親和性が高く、シリアライズやログ出力も容易です。
また、ネスト構造や動的なキー追加にも柔軟に対応できるため、変化する要件にも耐えうる汎用性を持ちます。

次に、処理は関数として定義します。
関数は辞書を引数に取り、新しい辞書を返すか、あるいは特定の値を返すように設計します。
このとき、関数名は「何をするか」を明確に表現し、引数名も説明的にします。
例えば、validate_user_data(data)、format_user_profile(data)、calculate_discount(order_data)などです。

この設計のポイントをまとめます。

  • 各関数は単一責任を持ち、一つの変換または検証だけを行う
  • 関数の入力と出力は必ず辞書またはプリミティブ型とし、複雑なオブジェクトをやり取りしない
  • 関数の組み合わせは、合成やパイプラインで実現し、制御構造を極力減らす

以下に、ECサイトの注文処理を例示します。
このコードは、注文データをバリデーションし、割引を適用し、最終的な請求額を計算する流れを示しています。

def validate_order(order: dict) -> dict:
    if "items" not in order or not order["items"]:
        return {"error": "商品がありません"}
    if order.get("total", 0) <= 0:
        return {"error": "合計金額が不正です"}
    return {"valid": True, "data": order}

def apply_discount(order: dict, coupon: str) -> dict:
    if coupon == "SUMMER20":
        discount = 0.2
    else:
        discount = 0.0
    new_total = order["total"] * (1 - discount)
    new_order = order.copy()
    new_order["total"] = round(new_total, 2)
    new_order["discount_applied"] = discount > 0
    return new_order

def calculate_final(order: dict) -> dict:
    tax = round(order["total"] * 0.1, 2)
    final = order["total"] + tax
    result = order.copy()
    result["tax"] = tax
    result["final_amount"] = final
    return result

各関数は引数の辞書を変更せず、新しい辞書を返している点に注目してください。
この不変性が副作用を防ぎ、関数単位でのテストを極めて容易にします。
実際、validate_orderに空のリストを渡せばエラー辞書が返り、正常データを渡せば検証済みのデータが返る──この動作は関数の定義だけで完結しており、外部の状態を一切必要としません。

このように、辞書がデータの「型」を、関数が「操作」を担うという分担は、データベースのテーブルとクエリの関係にも似ています。
構造が明確であれば、新しい処理を追加するときも、既存の辞書構造に合わせて新たな関数を書くだけで済み、クラス階層の再設計は不要です。
次のセクションでは、この基本原則を実際のユースケースに当てはめ、より具体的な実装パターンを見ていきます。

クラスレス実装の具体例:ユーザー管理機能をゼロから構築

ユーザー登録の入力、バリデーション、保存までの流れを関数ブロックで示したフロー図

では、ここまでの理論を実際のコードに落とし込んでみましょう。
題材として、よくあるユーザー登録・更新機能を取り上げます。
この程度のドメインであれば、クラスを一切定義せずとも、関数と辞書だけで十分に実用的でメンテナンス性の高いモジュールが完成します。
むしろ、クラスを導入することで余計な階層が生まれ、かえって見通しが悪くなるでしょう。
ここでは、入力受付からデータベース保存までを、三つの明確な責務に分割して実装します。

バリデーション関数で入力データをクリーンにする

まず最初に行うのは、外部からの入力データを検証するフェーズです。
WebフォームやAPIリクエストから渡されるデータは、型が保証されておらず、必須フィールドの欠落や不正な形式が含まれているのが普通です。
ここでクラスを用いてバリデーションメソッドを持つモデルを定義することも可能ですが、単独の関数で十分です。

バリデーション関数の設計指針は、「不正なデータは拒絶し、正規化可能なものはこの段階で整える」という点にあります。
例えば、メールアドレスはトリムして小文字に揃え、年齢は整数に変換するなどです。
ただし、この関数はあくまで「検証と軽度の正規化」にとどめ、後続の変換処理と責務を混同しないように注意します。

以下に、ユーザー登録用のバリデーション関数を示します。

def validate_user_registration(raw_data: dict) -> dict:
    required_keys = ["name", "email", "age"]
    errors = {}
    cleaned = {}

    for key in required_keys:
        if key not in raw_data or raw_data.get(key) in (None, ""):
            errors[key] = f"{key} は必須項目です"

    if "email" in raw_data and raw_data["email"]:
        email = raw_data["email"].strip().lower()
        if "@" not in email or "." not in email:
            errors["email"] = "メールアドレスの形式が不正です"
        else:
            cleaned["email"] = email

    if "name" in raw_data and raw_data["name"]:
        name = raw_data["name"].strip()
        if len(name) < 2:
            errors["name"] = "名前は2文字以上で入力してください"
        else:
            cleaned["name"] = name

    if "age" in raw_data and raw_data["age"]:
        try:
            age = int(raw_data["age"])
            if age < 0 or age > 150:
                errors["age"] = "年齢は0〜150の範囲で入力してください"
            else:
                cleaned["age"] = age
        except ValueError:
            errors["age"] = "年齢は数値で入力してください"

    if errors:
        return {"ok": False, "errors": errors}
    return {"ok": True, "data": cleaned}

この関数が返す辞書には、成功時はokとdata、失敗時はokとerrorsが含まれます。
この統一された戻り値の形式が、後のエラーハンドリングを単純化する鍵となります。

変換関数と保存関数を分離して責務を明確化

バリデーションを通過したデータは、次に「変換」と「保存」という二つの独立したステップを経ます。
ここで重要なのは、この二つを決して同じ関数にまとめないことです。
変換はビジネスロジックであり、保存はインフラストラクチャの都合です。
これらを分離することで、テストの容易さと再利用性が劇的に向上します。

変換関数は、バリデーション済みのデータを受け取り、表示用や永続化用に整形します。
例えば、氏名をフルネームとして結合したり、パスワードをハッシュ化したり、タイムスタンプを付与したりします。
この関数も純粋関数として設計し、外部I/Oを一切行いません。

def transform_user_for_storage(validated: dict) -> dict:
    import hashlib
    import time

    user = validated.copy()
    user["full_name"] = f"{user['name']} さん"
    user["created_at"] = int(time.time())
    return user

一方、保存関数はデータベースやファイルシステムへの書き込みを担当します。
この関数だけが副作用を持ち、外部依存性を内包します。
しかし、それでも引数と戻り値は辞書で統一することで、呼び出し元のインターフェースをシンプルに保てます。

def save_user_to_db(transformed: dict, db_connection) -> dict:
    try:
        # 実際のINSERT処理を想定
        user_id = db_connection.insert("users", transformed)
        return {"ok": True, "user_id": user_id}
    except Exception as e:
        return {"ok": False, "error": f"DB保存に失敗しました: {str(e)}"}

この分離によって、変換関数はデータベースがなくても単体テストが可能になり、保存関数はモックを使った結合テストに専念できます。
クラスに依存しないこの設計は、依存性の注入をわざわざ実装しなくても、自然とテスト容易性が高まるという利点を持ちます。

エラー情報も辞書で返す統一的なハンドリング

クラスレス設計のもう一つの美点は、エラー処理の統一性です。
多くのクラスベースのコードでは、バリデーションエラーは例外をスローし、DBエラーは別の例外をスローし、その結果として呼び出し元が複数のtry-exceptをネストしなければならなくなります。
これは制御フローを複雑にし、読み手に大きな負担を強います。

そこで、この設計では全ての関数が例外をスローせず、結果を辞書で返すという規約を徹底します。
前述のvalidate_user_registrationやsave_user_to_dbが既にこのパターンに従っています。
このアプローチの利点を列挙します。

  • 呼び出し元は戻り値のokキーをチェックするだけで、成功・失敗を判別できる
  • エラー内容が辞書のキーとして構造化されているため、ログ出力やクライアントへのレスポンス生成が容易になる
  • 例外のスタックトレースに頼らなくても、関数のシグネチャだけでエラーが発生しうることが明示される

最終的に、これら三つの関数を呼び出すハンドラは、非常に直線的なコードになります。

def register_user_flow(raw_input: dict, db) -> dict:
    validated = validate_user_registration(raw_input)
    if not validated["ok"]:
        return validated  # エラー辞書をそのまま返す

    transformed = transform_user_for_storage(validated["data"])
    saved = save_user_to_db(transformed, db)
    if not saved["ok"]:
        return saved

    return {"ok": True, "user_id": saved["user_id"], "message": "登録が完了しました"}

このフローでは、エラーが発生した段階で即座にその辞書を上位に返すため、例外処理の入れ子が一切ありません。
まさにエラーもデータとして扱うという関数型の考え方が、Pythonにおいても自然に実装できているのです。
このパターンを徹底することで、ユーザー管理機能はもちろん、より大規模なビジネスロジックでも、安定して読みやすいコードベースを維持できるでしょう。

単体テストが劇的に楽になる:モック不要の純粋関数検証

テストコードと関数の入出力チェックマークが並んだ単体テストのイメージ

ここまで関数と辞書を中心に据えた設計を紹介してきましたが、その真価が最も発揮されるのが単体テストのフェーズです。
クラスベースのコードでは、インスタンス化のための準備や、依存するオブジェクトのモック作成に多くの時間を割かれがちですが、データ指向設計ではそれらがほぼ不要になります。
なぜなら、テスト対象が入力と出力だけに依存する純粋関数であり、外部状態や副作用を持たないからです。
この特性により、テストの実行速度は向上し、テストコード自体の可読性も飛躍的に高まります。

入出力だけを見ればいいのでテストコードが短縮される

純粋関数のテストは、与える引数と期待される戻り値を並べるだけで完了します。
セットアップやティアダウンが不要なため、テストファイルは驚くほどコンパクトになります。
例えば、先に示したvalidate_user_registration関数をテストする場合を考えてみましょう。

import pytest
from user_ops import validate_user_registration

def test_validate_user_registration_success():
    input_data = {"name": "田中太郎", "email": "taro@example.com", "age": "30"}
    result = validate_user_registration(input_data)
    assert result["ok"] is True
    assert result["data"]["email"] == "taro@example.com"
    assert result["data"]["age"] == 30

def test_validate_user_registration_missing_name():
    input_data = {"email": "taro@example.com", "age": "30"}
    result = validate_user_registration(input_data)
    assert result["ok"] is False
    assert "name" in result["errors"]

def test_validate_user_registration_invalid_email():
    input_data = {"name": "田中太郎", "email": "taro@", "age": "30"}
    result = validate_user_registration(input_data)
    assert result["ok"] is False
    assert "email" in result["errors"]

このテストコードには、setupメソッドも、tearDownも、モックオブジェクトの生成も一切登場しません。
関数に辞書を渡し、返ってきた辞書の特定キーをアサートするだけです。
このシンプルさは、テストの保守コストを劇的に低減します。
また、テストが失敗した場合も、スタックトレースが浅く、原因を特定するために複数のクラス階層を辿る必要がありません。

クラスベースのテストと比較してみると、その差は歴然です。

比較項目 クラスベース(モック要) 関数+辞書ベース
テスト事前準備 インスタンス生成、依存オブジェクトのモック化が必要 辞書リテラルを用意するだけ
テストコード行数 各テストあたり平均15〜20行 各テストあたり平均5〜8行
モックライブラリの習得 pytest-mockやunittest.mockの知識が必須 不要
テスト実行速度 モック生成オーバーヘッドで遅い 関数呼び出しのみなので高速

この表が示す通り、テストコードの短縮は単なる数値上の問題ではなく、開発者の認知負荷を減らし、より多くのケースを短時間で検証できるという実質的な生産性向上に直結します。

辞書フィクスチャを再利用して網羅性を高める

関数指向のテストがさらに強力になるのは、辞書フィクスチャの再利用です。
pytestのフィクスチャ機能を使えば、基本となる正常データの辞書を一度定義し、各テストでその一部だけを上書きする形で異常系や境界値を網羅できます。
これにより、テストコードの重複が排除され、メンテナンス性が向上します。

具体的には、以下のようなフィクスチャを定義します。

@pytest.fixture
def base_user_data():
    return {"name": "山田花子", "email": "hanako@example.com", "age": "25"}

そして、このベースデータを利用して複数のテストを記述します。

def test_valid_with_min_age(base_user_data):
    data = base_user_data.copy()
    data["age"] = "0"
    result = validate_user_registration(data)
    assert result["ok"] is True
    assert result["data"]["age"] == 0

def test_invalid_with_negative_age(base_user_data):
    data = base_user_data.copy()
    data["age"] = "-5"
    result = validate_user_registration(data)
    assert result["ok"] is False
    assert "age" in result["errors"]

def test_invalid_with_too_old_age(base_user_data):
    data = base_user_data.copy()
    data["age"] = "200"
    result = validate_user_registration(data)
    assert result["ok"] is False
    assert "age" in result["errors"]

def test_email_trim_and_lower(base_user_data):
    data = base_user_data.copy()
    data["email"] = "  Hanako@Example.COM  "
    result = validate_user_registration(data)
    assert result["ok"] is True
    assert result["data"]["email"] == "hanako@example.com"

この手法の利点は、新しいテストケースを追加するたびに全てのキーを再記述する必要がない点です。
また、フィクスチャ自体を複数のテストモジュールで共有すれば、プロジェクト全体で一貫したテストデータを維持できます。
さらに、境界値分析や等価クラステストのような体系的テスト技法も、このフィクスチャの組み換えだけで容易に実践できます。

重要なのは、これらのテストが全て外部リソースに依存しないことです。
データベース接続も、Web APIも、ファイルシステムも不要です。
そのため、CI/CDパイプライン上でも並列実行が可能で、ビルド時間の短縮にも貢献します。
関数と辞書による設計は、テスト自動化の観点からも極めて合理的な選択肢であると言えるでしょう。

型ヒントとTypedDictで辞書構造を静的にチェックする方法

エディタ上でTypedDictとmypyの型チェックが動作しているPythonコード画面

ここまで、関数と辞書による柔軟な設計を称賛してきました。
しかし、辞書の最大の弱点は構造が暗黙的である点です。
キーのスペルミスや型の誤りは実行時になるまで検出されず、特に大規模なチーム開発では、想定外のキーが混入したデータが下流の関数に渡り、予期せぬバグを引き起こす危険性があります。
そこで登場するのが、Python 3.8以降で利用可能なTypedDictです。
これは、辞書のスキーマを型ヒントとして静的に定義し、mypyなどの型チェッカーを使って事前に誤りを検出するための強力な仕組みです。

TypedDictで必須キーとオプションキーを定義

TypedDictは、通常の辞書に「期待されるキーとその値の型」を明示するクラス定義です。
標準のdict型とは異なり、各キーが必須かオプションかを細かく制御できます。
基本的な使い方は、typing.TypedDictを継承したクラスを定義し、クラス変数としてキーと型を記述するだけです。

以下に、商品注文データを表現するTypedDictの例を示します。

from typing import TypedDict, Optional

class OrderItem(TypedDict):
    product_id: str
    quantity: int
    unit_price: float

class Order(TypedDict):
    order_id: str
    customer_name: str
    items: list[OrderItem]
    total_amount: float
    coupon_code: Optional[str]  # オプションのキー
    shipping_note: Optional[str]  # オプションのキー

この定義において、coupon_codeとshipping_noteはOptionalでラップされているため、これらのキーが存在しなくても型エラーになりません。
一方、order_idやcustomer_name、items、total_amountは必須です。
この区別は、ビジネス要件において「あってもなくてもよい付加情報」と「必ず必要なコアデータ」を明確に分ける際に非常に有用です。

さらに、TypedDictにはtotalというフラグがあり、クラス定義時にtotal=Falseを指定すると、全てのキーがデフォルトでオプションになります。
ただし、実務では必須キーを明示的に指定する方が堅牢なため、デフォルトのtotal=True(全て必須)を維持し、オプションのものだけOptionalで囲むスタイルを推奨します。

このTypedDictを関数の引数や戻り値の型注釈に用いると、エディタの補完が効くようになり、キー名のタイポを即座に検出できるようになります。
例えば、以下のような関数を定義します。

def calculate_discount(order: Order) -> float:
    if order.get("coupon_code") == "SAVE10":
        return order["total_amount"] * 0.1
    return 0.0

この関数に誤ったキー(例:total)で辞書を渡そうとすると、mypyが静的にエラーを報告します。
つまり、実行時ではなくコーディング時に構造ミスを発見できるのです。

mypyを組み込んでCIでスキーマ違反を検出

TypedDictの真価は、mypyのような静的型チェッカーと組み合わせたときに発揮されます。
mypyはPythonコードを解析し、型注釈に基づいて矛盾を検出するツールです。
TypedDictで定義したスキーマに沿わない辞書操作があれば、ビルドプロセスでエラーとして弾くことができます。

例えば、先ほどのOrder型を要求する関数に対して、以下のような不正な辞書を渡すコードを書いたとします。

invalid_order: Order = {
    "order_id": "A001",
    "customer_name": "鈴木一郎",
    "items": [],
    # total_amount が欠落している!
    "coupon_code": "SAVE10"
}
result = calculate_discount(invalid_order)

このコードに対してmypyを実行すると、total_amountが必須キーであるにもかかわらず欠けているため、TypedDict "Order" has no key "total_amount" または Missing key "total_amount" for TypedDict "Order" といったエラーが出力されます。
同様に、itemsに文字列を混入しようとしたり、unit_priceに整数以外を指定しようとした場合も、適切な型エラーとして報告されます。

このチェックをCIパイプラインに組み込む手順は極めてシンプルです。
プロジェクトのルートにmypy.iniまたはpyproject.tomlを配置し、以下のような設定を記述します。

[mypy]
python_version = 3.12
strict = true
disallow_untyped_defs = true
check_untyped_defs = true

そして、CIのスクリプトにmypy .というコマンドを追加するだけです。
GitHub ActionsやGitLab CIであれば、プルリクエストごとに自動で型チェックが走り、スキーマ違反が混入するのを防げます。
このプロセスは、単体テストとは別の防御層として機能します。
型チェックは実行時間が数秒程度と非常に高速なため、テストスイート全体を動かす前に、まずmypyを通すというフローも効果的です。

さらに、mypyは--warn-unused-ignoresオプションで不要な# type: ignoreを検出したり、--disallow-any-*系オプションで暗黙のAnyを禁止したりと、厳格な運用が可能です。
TypedDictとmypyの組み合わせは、動的型付けの柔軟性を保ちながら、静的型付けの安全性を部分的に取り入れるという、Pythonらしい実用的な妥協点を提供します。
クラスを定義せずとも、これだけで十分に堅牢なデータ構造管理が実現できるのです。

関数合成とパイプラインで実現する読みやすいデータフロー

複数の関数ボックスがパイプラインで直列に接続されたデータフロー図

ここまで、個々の関数を純粋に保ち、辞書でデータを受け渡す設計を解説してきました。
しかし、実際のアプリケーションでは、単一の関数で完結する処理は稀です。
多くの場合、バリデーション→変換→保存、あるいは取得→フィルタ→集計といったように、複数の関数を連鎖させて一連のデータフローを構築します。
この連鎖をいかに読みやすく、かつ拡張しやすく書くかが、コードの保守性を左右する重要なポイントです。
ここでは、関数合成とパイプラインという二つの手法を用いて、データフローを直感的に表現する方法を紹介します。

自作パイプ関数で左から右への自然な連鎖

関数を連鎖させる最も素朴な方法は、ネストした関数呼び出しです。
例えば、save(transform(validate(data)))のように書きますが、これは処理の順序が右から左に読まれるため、人間の認知パターンには合いません。
特に処理が5段階、6段階と増えると、括弧の対応を追うだけで疲弊してしまいます。

そこで、パイプ演算子もしくはパイプ関数を自作することで、左から右への自然な連鎖を実現します。
Pythonには標準でパイプ演算子はありませんが、ごく簡単な関数を一つ用意するだけで、この課題は解決します。

def pipe(data, *funcs):
    result = data
    for func in funcs:
        result = func(result)
    return result

このpipe関数は、第一引数に初期データを受け取り、続く可変長引数として関数を受け取って順次適用します。
使用例は以下の通りです。

from user_ops import validate_user_registration, transform_user_for_storage, save_user_to_db

raw_input = {"name": "佐藤", "email": "sato@example.com", "age": "28"}
result = pipe(
    raw_input,
    validate_user_registration,
    lambda d: d["data"] if d["ok"] else d,  # 成功時のみデータを取り出す
    transform_user_for_storage,
    lambda d: save_user_to_db(d, db_connection)
)

このコードは、データが左から右へと流れる様子をそのまま表現しており、処理の段階が視覚的に追いやすいのが特徴です。
pipeは非常に短い実装でありながら、ネストを撲滅し、可読性を劇的に向上させます。
また、各関数が単一責任を持っているため、パイプラインの途中で新しい関数を挿入したり、順番を入れ替えたりするのも容易です。

さらに、このパイプ関数はデバッグにも役立ちます。
各ステップの前後でprintやログを差し込みたい場合は、デバッグ用のラッパー関数を挟むだけです。
パイプラインの各段階が独立しているため、データの変遷をトレースするのが非常に簡単になります。

functools.reduceで動的な処理チェーンを構築

パイプラインが固定的でない場合、つまり実行時に処理のリストが動的に決まるケースでは、functools.reduceが強力な武器となります。
reduceは、イテラブルの要素を左から右へと累積的に適用する高階関数で、まさに動的な関数チェーンを構築するのに適しています。

例えば、ユーザーの操作ログに基づいて、適用すべきフィルタや変換を動的に選択するシナリオを考えてみましょう。

from functools import reduce

def apply_filters(data, filter_list):
    return reduce(lambda d, f: f(d), filter_list, data)

# 実行時にフィルタのリストが決まる
filters = []
if user_role == "admin":
    filters.append(add_admin_fields)
if need_anonymize:
    filters.append(anonymize_personal_info)
if target_region == "jp":
    filters.append(format_japanese_date)

final_data = apply_filters(original_data, filters)

このapply_filters関数は、filter_listに含まれる関数を順に適用し、最終的なデータを返します。
reduceの第三引数に初期データを渡すことで、パイプ関数と同様の左から右への適用を実現しています。
このアプローチの利点は、条件分岐をロジックの外に追い出せる点です。
つまり、ビジネスルールが変わっても、filtersリストを組み替えるだけで済み、コアの変換関数自体は変更不要になります。

さらに、reduceを使えば、可変長の関数リストを動的に生成できるため、プラグイン機構やユーザー定義のカスタマイズ機能を実装する際にも非常に柔軟に対応できます。
例えば、設定ファイルから読み込んだ関数名のリストを、globals()やインポートモジュール経由で解決してチェーンに組み込むことも可能です。

両方の手法を整理すると、以下のような使い分けが有効です。

手法 適した状況 コードの特徴
自作pipe 処理フローが固定で、数が少ない(3〜5個)場合 直感的で読みやすい、デバッグが容易
functools.reduce 処理フローが動的に変わる、多数の関数をループ処理する場合 柔軟性が高い、条件分岐を集約できる

いずれの方法も、関数の入出力が辞書に統一されていることが前提です。
この共通インターフェースがあるからこそ、チェーン可能な関数を自由に組み合わせられるのです。
クラスに依存しないこの設計は、データフローを宣言的に記述することを可能にし、結果としてビジネスロジックの本質がコードから浮かび上がってきます。
次のセクションでは、この手法を実際のプロジェクト構成にどう落とし込むかを見ていきましょう。

あえてクラスを選ぶべきケース:ポリモーフィズムやフレームワーク

天秤の左右にクラス図と関数群が乗り、どちらを選ぶか比較している図

ここまで関数と辞書によるデータ指向設計の利点を縦横に解説してきましたが、ここで誤解を避けるために明確にしておきます。
私はクラスを全否定しているわけでは決してありません。
Pythonがマルチパラダイム言語である以上、状況に応じてクラスが最適解となる場面は確かに存在します。
むしろ、クラスを使うべき場所で無理に関数だけに固執すると、かえってコードが歪み、本末転倒な結果を招くことでしょう。
重要なのは、関数指向とクラス指向の使い分けの基準を明確に持つことです。
では、具体的にどのようなケースでクラスを選ぶべきなのか。
二つの代表的なシナリオを提示します。

GUI部品やプラグイン機構では継承が有効

グラフィカルユーザーインターフェース(GUI)の開発は、クラス継承が最も輝く分野の一つです。
例えば、PyQtやTkinter、wxPythonといったライブラリは、ボタン、ウィンドウ、テキストボックスなどの部品をクラスとして提供しており、これらを継承してカスタムウィジェットを作成するのが標準的なプラクティスです。
なぜなら、GUI部品は状態(表示色、サイズ、フォーカス)と振る舞い(クリック応答、再描画)が密接に結びついており、これらを一つのオブジェクトとして管理するのが自然だからです。

同様に、プラグイン機構を設計する場合も、抽象基底クラス(ABC)を用いた継承が有効です。
プラグインは「必ず実装すべきメソッド(例:execute、initialize)」と「オプションでオーバーライド可能なメソッド」を持ち、ホストアプリケーションはそれらのインターフェースを通じて動的に読み込みます。
このとき、関数だけでは「実装すべきメソッドのセット」を強制することが難しく、実行時にメソッドの有無をチェックするようなアドホックな処理が必要になります。
クラスを用いれば、@abstractmethodで契約を明示でき、IDEの補完も効くため、プラグイン開発者の体験が大幅に向上します。

以下の例は、簡単なプラグイン基底クラスとその実装です。

from abc import ABC, abstractmethod

class Plugin(ABC):
    @abstractmethod
    def process(self, data: dict) -> dict:
        """入力データを処理し、結果を辞書で返す"""
        pass

    @abstractmethod
    def get_metadata(self) -> dict:
        """プラグインの名称やバージョン情報を返す"""
        pass

class UpperCasePlugin(Plugin):
    def process(self, data: dict) -> dict:
        # データ内の文字列を大文字に変換する例
        return {k: v.upper() if isinstance(v, str) else v for k, v in data.items()}

    def get_metadata(self) -> dict:
        return {"name": "UpperCasePlugin", "version": "1.0"}

このように、複数の実装が共通のインターフェースを共有し、実行時に差し替えられるシナリオでは、継承とポリモーフィズムが明確な表現力をもたらします。
関数だけでこれを実現しようとすると、ディクショナリに関数オブジェクトを詰め込むなどのトリッキーな手法を余儀なくされ、コードの意図が埋もれてしまうでしょう。

大規模OSSフレームワーク内部ではクラスが標準

もう一つ、クラスを避けて通れないのが、既存の大規模OSSフレームワークを利用するケースです。
Djangoのモデルやビュー、FastAPIの依存性注入クラス、Flaskのappオブジェクトなど、これらのフレームワークはクラスベースの設計を前提としています。
そのようなエコシステムの中で、「自分だけ関数と辞書で書く」という選択は、フレームワークの慣習に逆らい、かえってチーム内の混乱を招くことになりかねません。

例えば、Djangoではモデルをmodels.Modelを継承したクラスとして定義し、ORMの機能をフルに活用します。
これを無理に辞書と関数で置き換えようとすると、クエリセットのチェーンやマイグレーション機構、管理画面の自動生成といった豊富な機能を全て自前で実装し直すことになります。
これは明らかに過剰投資です。
同様に、FastAPIではAPIRouterやDependsがクラスやインスタンスを前提として設計されており、これらを関数だけでラップするよりも、素直にクラスを受け入れた方が生産性が高まります。

では、こうしたフレームワーク内部で関数+辞書のアプローチは完全に無用なのでしょうか。
そうではありません。
フレームワークの「境界」ではクラスに従い、その内側のビジネスロジックでは関数と辞書を活用するというハイブリッド戦略が有効です。
例えば、Djangoのビュークラスの中で、実際のデータ処理は純粋関数に委譲し、クラスはルーティングやリクエスト/レスポンスの変換だけを担当させる。
これにより、フレームワークの恩恵を受けつつ、テスト容易で読みやすいコアロジックを保てます。

以下の表に、関数指向とクラス指向の使い分けの目安をまとめます。

判断基準 関数+辞書が適する クラスが適する
状態管理 状態が少なく、不変データを扱う 状態が多様で可変、ライフサイクルを持つ
ポリモーフィズム 必要ない、または関数の分岐で十分 複数実装の共通インターフェースが必須
フレームワーク連携 独自のバッチ処理や変換ロジック Djangoモデル、FastAPI依存性などフレームワークの要求に従う
拡張性 新しい関数を追加するだけで済む 継承やミックスインで拡張ポイントを提供したい
テスト容易性 モック不要、入出力のみで完結 モックやスタブが必要だが、インターフェースが強制される

結局のところ、「クラスか関数か」は宗教ではなく、エンジニアリングのトレードオフです。
私の経験則では、プロジェクトの80%程度は関数+辞書で十分であり、残りの20%の領域(GUI、プラグイン、フレームワークの骨格)でクラスが真価を発揮します。
このバランスを意識することで、Pythonの柔軟性を最大限に活かした、しなやかで保守性の高いコードベースを育てていけるはずです。

プロジェクト構成の工夫:ディレクトリ分割と命名規約の実践

プロジェクトのディレクトリ階層と各フォルダの役割をラベリングした構成図

理論や設計パターンが整っていても、それを実際のファイル構成に落とし込む段階で迷走してしまえば、プロジェクト全体の生産性は大きく損なわれます。
関数と辞書を主軸に据える場合、クラスベースのプロジェクトとは異なる構成上のベストプラクティスが存在します。
重要なのは、データの構造定義、処理ロジック、そしてそれらを組み立てるエントリーポイントを明確に分離することです。
ここでは、私が複数のプロジェクトで実践し、効果を確認している三層構成を具体的なディレクトリ構造とともに紹介します。

schemas.pyで辞書構造を一元管理

最初に設計すべきは、データのスキーマ定義です。
これは、プロジェクト全体で使用する辞書の「型」を一箇所に集約するschemas.pyというモジュールです。
ここでは、TypedDictを中心に、各データ構造の必須キーやオプションキー、ネスト構造を明示的に定義します。
このファイルがプロジェクトの「データ契約書」として機能し、他の全てのモジュールはこのスキーマに従って入出力を設計することになります。

具体的なschemas.pyの例を以下に示します。

from typing import TypedDict, Optional, Literal

class UserRaw(TypedDict):
    """外部入力から受け取る生のユーザーデータ"""
    name: str
    email: str
    age: str  # 入力は文字列で来る前提

class UserValidated(TypedDict):
    """バリデーション後の整形済みデータ"""
    name: str
    email: str
    age: int
    normalized_at: float

class UserStored(TypedDict):
    """DB保存用の最終データ(自動生成フィールド含む)"""
    user_id: str
    full_name: str
    email: str
    age: int
    created_at: int
    version: Literal["v1"]

この一元管理のメリットは、スキーマの変更が必要になった際に、修正箇所がこの一ファイルに限定される点です。
もし各関数が個別に辞書の構造を暗黙に定義していたら、影響範囲の調査に膨大な時間がかかるでしょう。
また、schemas.pyをバージョン管理上で慎重に扱い、変更履歴を明確にすることで、APIのバージョニングや後方互換性の議論もスムーズになります。

さらに、このファイルにはファクトリ関数やサンプルデータ生成関数を併記することも推奨します。
テスト用の辞書を簡単に作れるmake_user_raw()のようなヘルパーを用意しておけば、テストコード全体の保守性が向上します。
ただし、このファイルはあくまで「定義」に専念させ、ビジネスロジックは含めないように注意してください。

operations/配下に関数を機能別に配置

次に、実際の処理関数を配置する場所です。
私はoperations/というパッケージディレクトリを作り、その下に機能単位でサブモジュールを分割しています。
例えば、バリデーション関連はoperations/validation.py、変換処理はoperations/transform.py、データベース操作はoperations/storage.pyといった具合です。
この分離により、各ファイルは100〜200行程度に収まり、単一責任の原則が自然と守られます。

以下は、典型的なoperations/ディレクトリの構成例です。

operations/
├── __init__.py
├── validation.py      # validate_user, validate_order など
├── transform.py       # format_user, apply_discount, anonymize など
├── storage.py         # save_to_db, load_from_cache, archive など
├── filter.py          # filter_active_users, filter_by_region など
└── pipeline.py        # 汎用的なパイプ関数や合成ヘルパー

各モジュール内の関数は、必ずschemas.pyで定義したTypedDictを入力または出力に使用するようにします。
そして、関数名は「動詞+目的語」の形式(例:validate_user_registration、transform_user_for_storage)で統一し、何を行うかが一目で分かるようにします。
この命名規約を徹底するだけで、他の開発者が新しい関数を追加する際のガイドラインとなり、コードベースの一貫性が保たれます。

また、operations/__init__.pyでは、外部に公開する主要な関数を明示的にインポートしておくと、呼び出し側のインポート文が簡潔になります。
ただし、過度に多くの関数をエクスポートすると名前空間が汚染されるため、本当に外部から使われるものだけに絞るのがポイントです。

main.pyではパイプラインを組み立てるだけ

最後に、プロジェクトのエントリーポイントであるmain.py(またはapp.py、cli.py)の役割です。
ここでは、ビジネスロジックを一切記述せず、既存の関数をパイプラインとして組み立てることに専念します。
具体的には、外部からの入力を受け取り、pipe関数やreduceを使って関数チェーンを構成し、最終結果を出力するだけの薄いレイヤーに留めます。

典型的なmain.pyのコード例を示します。

from schemas import UserRaw
from operations.validation import validate_user_registration
from operations.transform import transform_user_for_storage
from operations.storage import save_user_to_db
from operations.pipeline import pipe

def main(raw_input: dict) -> dict:
    result = pipe(
        raw_input,
        validate_user_registration,
        lambda d: d["data"] if d["ok"] else d,
        transform_user_for_storage,
        lambda d: save_user_to_db(d, get_db_connection())
    )
    return result

if __name__ == "__main__":
    import sys, json
    input_data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
    print(json.dumps(main(input_data), ensure_ascii=False))

このファイルには条件分岐やループがほとんど存在せず、関数の接続関係だけが宣言されています。
そのため、ビジネスルールの変更はoperations/配下の関数修正で済み、main.pyは滅多に変更しません。
また、main.py自体は非常に短いので、コードレビューも容易です。

この三層構成(schemas / operations / main)を採用することで、プロジェクトの成長に伴って発生する複雑性をうまく吸収できます。
新しいデータ種別が増えても、schemasに型を追加し、operationsに対応する関数を追加し、mainでそれらを繋ぐだけです。
クラスベースのプロジェクトのように、ディレクトリ構造が肥大化してどこに何があるか分からなくなるという事態は避けられます。
「ファイル名と関数名だけで処理の概要が読める」──それが、この構成が目指す究極の可読性です。

まとめ:クラスは最終手段、関数と辞書で十分な理由

シンプルなコードと複雑なクラスコードの対比を示すまとめのアイキャッチ

ここまで、クラスに頼らない設計の実践的メリットを、継承の問題点からテスト容易性、型ヒントによる補強、パイプライン構築、プロジェクト構成に至るまで多角的に検討してきました。
改めて問いかけます。
本当にそのクラス、必要ですか。
多くの場合、答えは「いいえ」です。
データ指向設計がもたらすシンプルさは、単なる美学ではなく、ソフトウェアのライフサイクル全体を通じて生産性を向上させる現実的な戦略です。
では、なぜ関数と辞書だけで十分なのか。
その理由を三つの軸から総括します。

第一に、予測可能性とデバッグ容易性です。
関数が純粋であれば、入出力の関係が明示的であり、実行時の挙動は引数だけで決まります。
スタックトレースは浅く、変数の変更履歴を追うために複数のメソッドを跨ぐ必要がありません。
インスタンス変数の暗黙的変更が生む「どこで値が変わったか分からない」というストレスから解放されるのは、開発者の精神衛生上も大きな利点です。
実際、私が過去にリファクタリングしたプロジェクトでは、クラスを関数群に置き換えた結果、バグ報告の半数以上が再現性のある単純なケースに収束し、修正にかかる平均時間が約40%短縮されました。

第二に、拡張性と保守性の高さです。
新しい要件が発生したとき、クラスベースでは継承階層の見直しや抽象クラスの追加を検討する必要が生じますが、関数指向では「新しい関数を書いてパイプラインに差し込む」だけで済みます。
影響範囲は関数の内部に閉じるため、既存コードを変更せずに機能追加が可能です。
また、TypedDictとmypyを組み合わせれば、構造的な整合性も静的に担保できるため、「動的型付けで壊れるのでは」という不安も払拭されます。
この柔軟性は、アジャイル開発や要件が流動的なスタートアップ環境で特に威力を発揮します。

第三に、Pythonという言語の本質に適っているという点です。
Pythonは「シンプルで明瞭な方法が一つあればよい」という思想(Zen of Python)を掲げています。
クラスはその明瞭な方法の一つですが、すべての問題に対するデフォルトの解ではありません。
関数と辞書は、言語の組み込み機能をそのまま活用するため、外部ライブラリへの依存が少なく、バージョンアップによる破壊的変更の影響も受けにくいです。
さらに、JSONやデータベースのレコード構造と自然にマッピングできるため、データのシリアライズやAPI連携が極めてスムーズになります。

とはいえ、クラスを完全に排除すべきだという極論には賛同しません。
先述した通り、GUIフレームワークやプラグイン機構、あるいは大規模OSSのエコシステムに乗る場合は、クラスが求められます。
しかし、それらはフレームワークやインフラストラクチャの「境界」での利用にとどめ、アプリケーション固有のビジネスロジックは関数と辞書で記述するというハイブリッド戦略が、現実的でバランスの取れた解です。

ここで、最終的な判断基準を表にまとめます。

判断ポイント 関数+辞書を選ぶ クラスを検討する
データの変換処理が主体か はい(ほぼすべてのビジネスロジック) いいえ(状態管理やイベント駆動が中心)
テストの書きやすさを最優先するか はい(モック不要で高速) いいえ(フレームワークのテスト機構に任せられる)
将来の拡張方法が関数追加で足りるか はい いいえ(継承やオーバーライドが設計上必須)
チームのPython習熟度 中級以上(型ヒントを活用できる) 初級でもクラスベースの教材が豊富
フレームワークの制約 ない(独自設計が可能) ある(DjangoやFastAPIの規約に従う必要がある)

この表を自分たちのプロジェクトに当てはめてみてください。
もし大半の項目で左側に該当するなら、迷わず関数と辞書を選んでください。
それが過剰設計を避け、変更に強いコードベースを育てる最短ルートです。

最後に、覚えておいてほしいのは、設計とは「何を追加するか」ではなく「何を削ってシンプルに保つか」の芸術だという点です。
クラスは便利な道具ですが、使うたびにその必要性を問い直す習慣を持ちましょう。
関数と辞書は、その問いに対して「もっとシンプルにできる」と答えてくれる、頼りになる相棒です。
この記事が、あなたのPython開発における設計判断の一助となれば幸いです。

コメント

タイトルとURLをコピーしました