Python で if-else 連鎖を避けるレジストリパターン
本文の状態
日本語全文を表示中
詳細モードで約13分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
KDnuggets
Python コードベースにおける if-else チェーンの肥大化問題を解決し、拡張性と保守性を高めるためのレジストリパターンの実装方法と適用タイミングを解説する技術記事である。
AI深層分析を開く2026年8月6日 05:10
AI深層分析
キーポイント
if-else チェーンの欠陥分析
従来の条件分岐チェーンは Open/Closed Principle を違反し、新機能追加のために既存コードの改修を強いるため、保守性と拡張性を損なう。
レジストリパターンの定義と利点
辞書やクラスを用いたレジストリパターンは、ロジックを分離し、外部からの拡張を可能にするため、コードの複雑さを軽減する。
実装段階の解説
記事では簡易的な 5 行の辞書から始まり、本番環境で再利用可能なクラスへと発展させるための具体的なステップを提示している。
適用タイミングの判断基準
レジストリパターンがコードに価値をもたらすのは、分岐ロジックが特定の規模を超え、保守コストが顕著になった場合に限られる。
辞書による単純な置換の利点
条件分岐を辞書のキー参照に置き換えることで、探索が O(1) に短縮され、利用可能なオプションを動的に列挙可能になる。
重要な引用
software entities (classes, modules, and functions) should be open for extension but closed for modification
The unit of change should be 'add a file,' not 'modify the central dispatcher.'
Every new branch adds to the cognitive weight of the whole function.
The registry pattern fixes all four by flipping the relationship. Instead of the dispatcher knowing about every option, each option announces itself to the dispatcher.
編集コメントを表示
編集コメント
本記事は、コードの可読性と保守性を向上させるための具体的な設計手法を提示しており、中規模以上の Python プロジェクトに携わるエンジニアにとって有益な知見である。特に Open/Closed Principle の観点から、if-else チェーンがもたらすリスクを明確に指摘している点は評価できる。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
image**
# イントロダクション
Python のコードベースには、誰もが直面する共通の課題があります。最初は小さな関数から始まります。分岐が 2 つ、あるいは 3 つあるだけです。しかし、誰かがケースを追加し、別の人がさらに追加します。1 年後には、誰も手をつけたくない 200 行にも及ぶ if/elif/else の連鎖ができあがっています。以下にその例を示します。
def get_model(name):
if name == "logreg":
return LogisticRegression()
elif name == "random_forest":
return RandomForestClassifier()
elif name == "svm":
return SVC()
elif name == "xgboost":
return XGBClassifier()
# ... さらに 15 の分岐
else:
raise ValueError(f"Unknown model: {name}")もちろん、このコードは動作します。しかし、これは「オープン/クロージング原則」を犯しています。この原則とは、「ソフトウェアのエンティティ(クラス、モジュール、関数)は拡張に対して開放的であるべきだが、変更に対して閉鎖的であるべきだ」というものです。
この課題に対処するより良い方法があります。それがレジストリパターンです。この記事では、レジストリパターンの概要から、5 行の辞書から始めて本番環境で再利用可能なクラスへと発展させる具体的な手順、そして実際にコードに採用すべきタイミングについて解説します。それでは始めましょう。
# if-else チェーンの問題点
長い条件分岐の連鎖は、いくつかの特定の点で失敗します:
</article>
「if-else」の連鎖は、オープン/クローズド原則に違反します。新しいケースを追加するたびに、すでに動作していた関数を手直ししなければなりません。昨日まで正常に動作し、テスト済みだったコードが再び開かれ、再テストとレビューを余儀なくされます。変更の単位は「中央のディスパッチャーを修正する」のではなく、「ファイルを追加する」とすべきです。
また、無関係なロジックが一つの場所に積み重なってしまいます。例えば、支払いディスパッチャーがクレジットカード、PayPal、暗号資産の処理を担っている場合、互いに無縁な三つのドメインが一つの関数内で共有されることになります。elif の連鎖は、強制的にそれらを同じ部屋で共有させるようなものです。
スケーラビリティも著しく低下します。新しい分岐が増えるたびに、関数全体の認知負荷が高まります。20 個の分岐がある場合、デバッグ中に 3 つ目の分岐を確認するたびに、その 20 個すべてをスクロールして確認しなければなりません。
外部からの拡張も不可能です。get_model() の連鎖がハードコードされたライブラリを提供すれば、ユーザーは対応に窮します。彼らはモンキーパッチングやフォークなしでは独自のモデルを追加できません。ロジックが完全に閉じられてしまっているのです。
レジストリパターンは、この関係性を逆転させることで上記の 4 つの問題をすべて解決します。ディスパッチャーがすべての選択肢を知っているのではなく、各選択肢が自らディスパッチャーに自分を登録する形です。
レジストリパターンとは何か?
これは基本的に、キーからオブジェクト(関数、クラス、インスタンス)へのマッピングを行う中央のルックアップテーブルです。各オブジェクトは条件分岐にハードコードされるのではなく、自らを登録します。Python ではこのルックアップテーブルはほぼ常に辞書であり、「登録」は通常デコレータを用いて行われます。
# if-else から辞書へ移行する
最小限の改善策は、if-else の連鎖を辞書参照に置き換えることです。これだけで線形探索が不要になり、処理速度が向上します。
MODEL_REGISTRY = {
"logreg": LogisticRegression,
"random_forest": RandomForestClassifier,
"svm": SVC,
"xgboost": XGBClassifier,
}
def get_model(name):
try:
return MODEL_REGISTRY[name]
except KeyError:
raise ValueError(
f"Unknown model: {name!r}. "
f"Available: {list(MODEL_REGISTRY)}"
) from Noneこれはすでにレジストリの一種ですが、手動で管理しているだけです。ディスパッチ処理は O(1) で完了し、list(MODEL_REGISTRY) を使うことで利用可能なオプションを動的に確認できます。また、ディスパッチャー自体が変更されることもありません。
ただし、一つ課題が残っています。新しいモデルを追加するたびに、辞書の記述とファイルの先頭でのクラスインポートを手動で行う必要がある点です。これを改善し、各コンポーネントが自らを登録できるようにしましょう。
デコレータベースのレジストリ構築
これが日常で実際に使うバージョンになります。登録処理をデコレータに任せることで、関数やクラスは定義された場所で自らのキーを宣言します。
PAYMENT_HANDLERS = {}
def register(payment_type):
def decorator(func):
PAYMENT_HANDLERS[payment_type] = func
return func
return decorator
@register("credit_card")
def charge_credit_card(amount):
return f"Charged ${amount} to credit card"
@register("paypal")
def charge_paypal(amount):
return f"Charged ${amount} via PayPal"
@register("crypto")
def charge_crypto(amount):
return f"Charged ${amount} in crypto"def process_payment(payment_type, amount):
handler = PAYMENT_HANDLERS.get(payment_type)
if handler is None:
raise ValueError(f"Unknown payment type: {payment_type!r}")
return handler(amount)
このように変更すると、process_payment dispatcher はたったの 4 行で済みます。しかも、今後コードが肥大化することはありません。
Apple Pay のサポートを追加したい場合は、新しい関数を書くだけで OK です。その関数に @register("apple_pay") デコレータを貼り付け、好きなファイルに配置すれば完了です。
中央のリストを手動で編集する必要も、マージ競合を起こす心配も、テスト済みのコードを再度開く手間もありません。ハンドラは対応するキーのすぐ隣に存在するため、次の開発者がどこを探せばいいか迷うこともないでしょう。
# 再利用可能なレジストリクラスの構築
レジストリが 2 つや 3 つ溜まってくると、同じデコレータのボイラープレートを書くのが面倒になってきます。これを小さなクラスにラップすれば、衝突検出機能、より良いエラーメッセージ、そしてクリーンな API が無料で手に入ります。
class Registry:
"""名前からオブジェクトへの再利用可能なレジストリ。"""
def __init__(self, name):
self.name = name
self._registry = {}
def register(self, key):
def decorator(obj):
if key in self._registry:
raise KeyError(
f"{key!r} はすでに {self.name!r} に登録されています"
)
self._registry[key] = obj
return obj
return decorator
def get(self, key):
if key not in self._registry:
raise KeyError(
f"{key!r} not found in {self.name!r}. "
f"Available: {list(self._registry)}"
)
return self._registry[key]
def __contains__(self, key):
return key in self._registry
def keys(self):
return self._registry.keys()
この仕組みを使って、設定ファイルだけで駆動されるテキスト処理パイプラインを構築できます。
transforms = Registry("transforms")
@transforms.register("lowercase")
def to_lower(text):
return text.lower()
@transforms.register("strip")
def strip_whitespace(text):
return text.strip()
@transforms.register("remove_digits")
def remove_digits(text):
return "".join(c for c in text if not c.isdigit())
パイプラインはもはやデータそのものです。YAML ファイル、CLI 引数、あるいはデータベースのレコードから取得できます。
pipeline = ["strip", "lowercase", "remove_digits"]
text = " Order #4521 CONFIRMED "
for step in pipeline:
text = transforms.get(step)(text)
print(repr(text))
出力:**
'order # confirmed'
ここで、このパターンの真価が発揮されます。プログラムの動作がコードではなく、文字列のリストというデータによって記述されるようになったのです。パイプラインの順序を入れ替えたり、ステップを追加したり、設定ファイルを通じて非エンジニアに管理させたりすることがすべて容易になります。
# __init_subclass__ を使ったクラスの自動登録
**
クラスを登録するレジストリを使う場合、Python にはさらに洗練されたテクニックがあります。Python 3.6 から利用可能になった __init_subclass__ フックは、サブクラスが定義されるたびに自動的に発火します。そのため、デコレータを使わずにサブクラスが自らをレジストリに登録できます。
class DataLoader:
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
super().__init_subclass__(**kwargs)
if fmt:
DataLoader._registry[fmt] = cls
@classmethod
def get_loader(cls, fmt):
if fmt not in cls._registry:
raise ValueError(
f"No loader for {fmt!r}. "
f"Available: {list(cls._registry)}"
)
return cls._registry[fmt]
class CSVLoader(DataLoader, fmt="csv"):
def load(self, path):
return f"Loading CSV from {path}"
class JSONLoader(DataLoader, fmt="json"):
def load(self, path):
return f"Loading JSON from {path}"
class ParquetLoader(DataLoader, fmt="parquet"):
def load(self, path):
return f"Loading Parquet from {path}"
loader = DataLoader.get_loader("parquet")
print(loader.load("sales.parquet")) # Loading Parquet from sales.parquetどこにもデコレータはありません。fmt= 引数付きで DataLoader をサブクラス化すれば、新しいクラスが自動的に登録されます。多くのフレームワークが、内部でプラグインシステムを構築する際にこの手法を採用しています。
# レジストリパターンが実務で役立つ理由
これは学術的なお遊びではありません。あなたがすでに使っているツールの基盤となっている技術です。
機械学習の実験設定では、Hugging Face Transformers、Detectron2、MMDetection といった主要ライブラリがすべてレジストリパターンを採用しています。これにより、YAML ファイル内でモデルやオプティマイザー、データ拡張手法を文字列名で指定できるようになります。
例えば build_model({"model": "resnet50"}) のように記述するだけで、巨大な「if backbone == ...」という条件分岐ブロックを回避でき、研究者がトレーニングコードに手を加えることなく新しいアーキテクチャを追加可能になります。
ファイル形式やパーサーのディスパッチにおいても同様の利点があります。拡張子("csv"、"json"、"parquet" など)を読み込みクラスにマッピングする仕組みです。新しいフォーマットに対応する際は、既存のローダーを編集するのではなく、新しいクラスを一つ追加するだけで済みます。
Web フレームワークのルーティングもレジストリの一種です。Flask の @app.route("/users") や Click の @cli.command() がその典型で、デコレータが URL やコマンド名を実際に処理する関数にマッピングしています。
プラグインアーキテクチャにおいても、「フォルダにファイルを置くだけで動作する」システム(pytest のフィクスチャ、Airflow のオペレーター、シリアライザーのバックエンドなど)は、ほとんどがインポート時にコンポーネントを集約するレジストリです。
イベントハンドラや状態機械でも、イベント名や状態を条件分岐で処理するのではなく、ハンドラ関数にマッピングします。これにより、複雑な条件文の入れ子(ネスト)ではなく、読みやすい辞書形式の遷移テーブルとして実装できます。
# 実践的な考慮事項と注意点
登録はインポート時にのみ行われます。デコレータが動作するのは、Python がそのファイルを実行した瞬間です。もしハンドラが handlers/apple_pay.py に置かれていて、そのモジュールを一度もインポートしない場合、@register デコレータは実行されず、ハンドラは静かに見失われてしまいます。解決策は、登録用モジュールが確実にインポートされるようにすることです。通常はパッケージの __init__.py で明示的にインポートするか、plugin フォルダ内のすべてのファイルをインポートする小さな発見ループを pkgutil.iter_modules とともに使用します。
沈黙した上書きを防ぎましょう。単純な辞書では、2 つのコンポーネントが同じキーに登録すると、互いに上書きしてしまいますが、誰も気づきません。上記の Registry クラスのように、重複するキーに対して例外を発生させることで、混乱を招くランタイムエラーを、インポート時に明確なエラーとして検出できるようにします。
利用可能な機能を明示しましょう。常に list(registry.keys()) でキーを公開し、エラーメッセージに含めてください。「Unknown model: 'lgbm'. Available: ['logreg', 'random_forest', 'xgboost']」というメッセージは、単なる KeyError を表示するよりも、はるかに多くのデバッグ時間を節約します。
使いすぎないでください。ロジックが明確に異なる 2〜3 の安定したブランチがある場合や、共通のシグネチャがない場合、あるいは条件が離散的なキーではなく範囲(if score > 0.9 ... elif score > 0.5 ...)である場合は、単純な条件分岐の方が明瞭です。レジストリが真価を発揮するのは、特定の状況だけです。つまり、互換性のある振る舞いを離散的なキーに基づいてディスパッチし、その振る舞いのセットが増えることを期待している場合です。
# Wrapping Up
レジストリパターンは、成長し続ける中央集権的で拡張が難しい if/elif/else の連鎖を、コンポーネント自身が埋めていく参照テーブルに置き換える手法です。そのメリットは具体的で、ディスパッチャー(処理振り分け部)の変更頻度が減ります。新機能も既存ファイルの修正ではなく、新しいファイルとして追加されるようになります。振る舞いを設定ファイルから制御可能になり、コードの利用者にとっては拡張可能なポイントが提供され、閉ざされた扉を避けることができます。
まずは小さく始めてみましょう。次回「kind == "a"」のような条件分岐を3 回目以降に書く瞬間に気づいたら、一瞬立ち止まって「辞書(dictionary)で代用できないか?」と自問してみてください。多くの場合、それで十分です。そこからデコレータ版やクラス版への移行もさほど遠くありません。
Before
if kind == "a": ...
elif kind == "b": ...
elif kind == "c": ...
After
@registry.register("a")
def handle_a(): ...
4 行のディスパッチャーを眺めるだけで済む未来の自分にとって、200 行に及ぶ条件分岐の迷路は不要です。
Kanwal Mehreen は機械学習エンジニアであり技術ライターで、データサイエンスと AI と医療の交差点に深い情熱を持っています。共著書に『ChatGPT で生産性を最大化する』があります。2022 年 APAC グーグル・ジェネレーション・スカラーとして、多様性と学術的卓越性の推進に尽力しています。また、Teradata テックにおけるダイバーシティ・スカラー、Mitacs Globalink リサーチ・スカラー、ハーバード大学 WeCode スカラーとしても認定されています。STEM 分野の女性を支援する団体 FEMCodes を設立するなど、変革を強く提唱し続けています。
AI算出
技術分析ainew評価限定的
AI モデルの選定ロジックに関する一般的なソフトウェア設計パターンの紹介であり、特定の最新 AI 製品や研究結果を報じていないため関連性は中程度。既存のベストプラクティスを解説する内容であり新規性は低く、検索意図も抽象的な技術用語に留まる。
6つの評価軸を見る
- AI関連度
- 50
- 情報源の信頼性
- 50
- 新規性
- 25
- 調べる価値
- 25
- 重複の少なさ
- 100
- 日本での有用性
- 25
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み