Python で if-else 連鎖を避けるレジストリパターン
KDnuggets は、Python 開発者に対し、可読性と保守性を向上させるため、複雑な if-else 連鎖の代わりにレジストリパターンを採用するよう提案している。
キーポイント
if-else 連鎖の課題解消
多数の条件分岐を含む if-else 構造はコードの可読性を低下させ、保守コストを増大させるため、代替手法が求められている。
レジストリパターンの実装
辞書や関数マップを用いて条件と処理をマッピングする「レジストリパターン」により、分岐ロジックをデータ駆動的に記述できる。
拡張性と保守性の向上
新しい機能の追加時に既存の分岐文を書き換える必要がなくなり、コードの閉鎖性・開放性原則(OCP)に沿った設計が可能になる。
重要な引用
Stop Using If-Else Chains: Use the Registry Pattern in Python Instead
Registry pattern allows you to map conditions to functions or classes dynamically.
影響分析・編集コメントを表示
影響分析
この記事は、特に大規模なビジネスロジックを扱う Python 開発者にとって、コード品質を向上させるための具体的なリファクタリング手法を提供するものである。if-else の多用による技術的負債を防ぎ、システム全体の保守性を高めるための標準的なプラクティスとして定着する可能性がある。
編集コメント
本記事で提案されているレジストリパターンは、AI モデルの推論パイプラインやラッパー関数を実装する際にも応用可能な、汎用的かつ強力な設計思想です。複雑な分岐ロジックを整理する手法として、開発現場での実践的価値が高いと言えます。

イントロダクション
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 チェーンの問題点
長い条件分岐の連鎖は、いくつかの特定の点で機能しません:
必ず JSON 形式で返してください。translation フィールドのみ。他のフィールドは一切追加しないこと — 余計なフィールドを書こうとして本文がトークン上限で打ち切られる事故を防ぐため:
{"translation": "
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 チェーンの問題点
長い条件分岐の連鎖は、いくつかの特定の点で機能しません:"}
これは「オープン・クローズド原則」に違反しています。新しいケースを追加するたびに、すでに動作していた関数を変更する必要が生じます。昨日テスト済みのコードを再び開き、再テストし、レビューし直すことになります。変更の単位は「中央のディスパッチャーを修正する」のではなく、「ファイルを追加する」とすべきです。
関連のないロジックが一つの場所に積み重なってしまいます。例えば、支払いディスパッチャーがクレジットカード、PayPal、暗号資産の 3 つを扱っているとしましょう。これらは互いに無関係なドメインですが、1 つの関数で共有されてしまいます。elif の連鎖は、強制的にそれらを同じ部屋(関数)に押し込むことになります。
スケーラビリティも著しく低下します。新しい分岐が増えるたびに、関数全体の認知負荷が高まります。20 個の分岐がある場合、デバッグ中に 3 番目の分岐を確認するたびに、その 20 個すべてをスクロールして確認しなければなりません。
外部からの拡張も不可能です。get_model() のチェーンがハードコードされたライブラリを提供し、ユーザーに渡したとしましょう。ユーザーは自分独自のモデルを追加できません。モナキーパッチングやフォークなしでは対応できないのです。ロジックが完全に封鎖されてしまっています。
レジストリパターンはこの 4 つの問題をすべて解決します。関係性を逆転させることで実現するのです。ディスパッチャーがすべての選択肢を知っているのではなく、各選択肢が自らディスパッチャーに自分を登録する形に変えるのです。
レジストリパターンとは何か?
これは基本的に、キーからオブジェクト(関数、クラス、インスタンス)へのマッピングを行う中央のルックアップテーブルです。ここで重要なのは、オブジェクトが条件分岐の中にハードコードされるのではなく、自らを登録する点です。Python では、このルックアップテーブルはほぼ常に辞書(dictionary)であり、「登録」処理は通常デコレータを使って行われます。
# 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これはすでにレジストリ(登録管理)ですが、手動で維持するタイプです。dispatch の計算量は 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 のディスパッチャーはたった 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 はさらに洗練されたテクニックを持っています。
__init_subclass__フック(Python 3.6 から利用可能)は、サブクラスが定義されるたびに自動的に発火します。この仕組みを使えば、デコレータは一切不要で、サブクラス自身がレジストリに登録されます。
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どこにもデコレータはありません。DataLoaderをサブクラス化し、fmt=引数を与えるだけで新しいクラスが自動的に登録されるのです。これが多くのフレームワークで、裏側でプラグインシステムを構築する際の手法となっています。
# 実務におけるレジストリパターンの活用場所
これは単なる学術的な演習ではありません。あなたが普段使っているツールの基盤となっている技術です。
- 機械学習の実験設定において、Hugging Face Transformers、Detectron2、MMDetection といった主要ライブラリはすべてレジストリパターンを採用しています。これにより、YAML ファイル内でモデルやオプティマイザー、データ拡張手法を文字列名で指定できるようになります。例えば
build_model({"model": "resnet50"})という記述は、巨大なif backbone == ...の連鎖に取って代わり、研究者がトレーニングコードに触れることなく新しいアーキテクチャを追加可能にします。
- ファイル形式とパーサーのディスパッチでは、「csv」「json」「parquet」などの拡張子を対応するローダークラスにマッピングします。新しいフォーマットに対応する際も、既存のローダーを編集するのではなく、新しいクラスを 1 つ追加するだけで済みます。
- Web フレームワークのルーティングも同様です。Flask の
@app.route("/users")や Click の@cli.command()は、裏側でレジストリとして機能しています。デコレータが URL やコマンド名を処理する関数に紐付ける仕組みです。
- プラグインアーキテクチャもその典型です。「このフォルダにファイルを置くだけで動作する」システムは、pytest のフィクスチャ、Airflow のオペレーター、シリアライザーのバックエンドなど、インポート時にコンポーネントを収集するレジストリであるケースがほとんどです。
- イベントハンドラや状態マシンでも同様のアプローチが取れます。イベント名や状態に対して分岐処理を行うのではなく、対応するハンドラ関数にマッピングします。これにより、複雑な条件文の入れ子構造は、読みやすい辞書形式の遷移テーブルへと置き換わります。
実装における注意点と留意点
- 機械学習の実験設定において、Hugging Face Transformers、Detectron2、MMDetection といった主要ライブラリはすべてレジストリパターンを採用しています。これにより、YAML ファイル内でモデルやオプティマイザー、データ拡張手法を文字列名で指定できるようになります。例えば
build_model({"model": "resnet50"})という記述は、巨大なif backbone == ...の連鎖に取って代わり、研究者がトレーニングコードに触れることなく新しいアーキテクチャを追加可能にします。
- ファイル形式とパーサーのディスパッチでは、「csv」「json」「parquet」などの拡張子を対応するローダークラスにマッピングします。新しいフォーマットに対応する際も、既存のローダーを編集するのではなく、新しいクラスを 1 つ追加するだけで済みます。
- Web フレームワークのルーティングも同様です。Flask の
@app.route("/users")や Click の@cli.command()は、裏側でレジストリとして機能しています。デコレータが URL やコマンド名を処理する関数に紐付ける仕組みです。
- プラグインアーキテクチャもその典型です。「このフォルダにファイルを置くだけで動作する」システムは、pytest のフィクスチャ、Airflow のオペレーター、シリアライザーのバックエンドなど、インポート時にコンポーネントを収集するレジストリであるケースがほとんどです。
- イベントハンドラや状態マシンでも同様のアプローチが取れます。イベント名や状態に対して分岐処理を行うのではなく、対応するハンドラ関数にマッピングします。これにより、複雑な条件文の入れ子構造は、読みやすい辞書形式の遷移テーブルへと置き換わります。
実装における注意点と留意点
登録処理はモジュールのインポート時にのみ実行されます。デコレータは、Python がそのファイルを実行する瞬間に動作します。もしハンドラが handlers/apple_pay.py に置かれていて、そのモジュールを一度もインポートしない場合、@register デコレータは発火せず、ハンドラは静かに見失われたままになります。
この問題を解決するには、登録用のモジュールが確実にインポートされるようにする必要があります。通常は、パッケージの __init__.py で明示的にインポートするか、プラグインフォルダ内のすべてのファイルをインポートする小さな検出ループ(pkgutil.iter_modules を使用)を実装します。
静かなる上書きを防ぎましょう。単純な辞書を使うと、2 つのコンポーネントが同じキーを登録した際に、互いに上書きし合って警告も出さず、バグに気づかれないことがあります。上記の Registry クラスのように、重複するキーに対して例外を発生させることで、わかりにくいランタイムエラーを、インポート時に明確なエラーとして検出できるようにできます。
利用可能な機能を明示しましょう。常に list(registry.keys()) でキー一覧を取得し、エラーメッセージに含めるようにします。「不明なモデル:'lgbm'。利用可能:['logreg', 'random_forest', 'xgboost']」というメッセージは、単なる KeyError を表示するよりも、はるかにデバッグ時間を節約できます。
使いすぎないでください。ロジックが明確に異なる 2〜3 の安定した分岐しかない場合や、共通のシグネチャがない場合、あるいは条件が離散的なキーではなく範囲(if score > 0.9 ... elif score > 0.5 ...)である場合は、単純な条件分岐の方が明瞭です。レジストリパターンが真価を発揮するのは、特定の状況だけです。それは「離散されたキーに基づいて、相互に交換可能な振る舞いを呼び出す」場合であり、かつその振る舞いのセットが増えることを期待している場合に限定されます。
まとめ
レジストリパターンは、成長して中央集権化し、拡張が困難になる if/elif/else の連鎖を、コンポーネント自身が埋めていく参照テーブルに置き換える手法です。そのメリットは具体的で、ディスパッチャー(分岐処理)の修正頻度が減ります。新機能も既存ファイルへの編集ではなく、新しいファイルとして追加されるようになります。動作設定は設定ファイルから制御可能になり、コードの利用者にとっては拡張可能なポイントが提供され、閉ざされた扉に直面することはありません。
まずは小さく始めてみましょう。次は「elif name == ...」とタイプし始めたら、一度立ち止まって「辞書(dictionary)で代用できないか?」と自問してみてください。多くの場合、それで十分です。そこからデコレータ版やクラス版への移行もさほど遠い道のりではありません。
Before
if kind == "a": ...
elif kind == "b": ...
elif kind == "c": ...
After
@registry.register("a")
def handle_a(): ...
将来の自分が、200 行にも及ぶ複雑な分岐ロジック(ladder)ではなく、たった 4 行のディスパッチャーをスルーする場面に出会ったとき、きっと感謝することになるでしょう。
Kanwal Mehreen は機械学習エンジニアであり技術ライターです。データサイエンスと AI と医療の交差点に深い情熱を抱いています。共著書に「ChatGPT で生産性を最大化する」があります。2022 年の Google Generation Scholar(APAC)として、多様性と学問的卓越性を提唱しています。また、Teradata の技術分野におけるダイバーシティ・スカラー、Mitacs Globalink リサーチ・スカラー、Harvard WeCode スカラーとしても認定されています。Kanwal は変化の熱心な支持者であり、STEM 分野での女性を支援する「FEMCodes」を設立しました。
原文を表示

**
# Introduction
Every Python codebase has this problem. A function that starts small. Two branches, maybe three. Then someone adds a case, someone else adds another, and a year later you've got 200 lines of if/elif/else that nobody wants to touch. Here's an example:
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 more branches
else:
raise ValueError(f"Unknown model: {name}")And yeah, it works. But it also breaks the Open/Closed Principle, which states that software entities (classes, modules, and functions) should be open for extension but closed for modification. There is a better way to handle this problem: the registry pattern**. This article covers what the registry pattern is, how to build it up from a five-line dictionary to a production-grade reusable class, and when it actually earns its place in your code. So, let's get started.
# The Problem With If-Else Chains
**
A long conditional chain fails in a few specific ways:
- It violates the Open/Closed Principle. New case, new edit to a function that already worked. Yesterday's tested code gets cracked open, retested, and reviewed again. The unit of change should be "add a file," not "modify the central dispatcher."
- It piles unrelated logic into one place. Say your payment dispatcher covers credit cards, PayPal, and crypto. Now three domains that have nothing to do with each other are sharing one function. The elif ladder forces them to share a room anyway.
- It scales badly. Every new branch adds to the cognitive weight of the whole function. Twenty branches is twenty things to scroll past every time you are debugging branch number three.
- It cannot be extended from outside. Ship a library with a hardcoded get_model() chain and your users are stuck. They cannot add their own model without monkey-patching or forking. The logic is sealed shut.
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.
What is the registry pattern?**
It is basically a central lookup table that maps keys to objects (functions, classes, instances), where each object registers itself instead of being hardcoded into some conditional. In Python, that lookup table is almost always a dictionary, and "registering" is usually done with a decorator.
# Going From If-Else to a Dictionary
**
The smallest possible win is to swap the chain for a dictionary lookup. One step, and the linear scan is gone:
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 NoneThis is already a registry — just a hand-maintained one. Dispatch is O(1), the options are introspectable with list(MODEL_REGISTRY), and the dispatcher never changes. One wart remains: every new model still means editing the dict and importing its class at the top of the file. You can do better by letting each component register itself.
# Building a Decorator-Based Registry
This is the version you'll actually use day to day. Registration happens in a decorator, so every function or class declares its own key right where it is defined:
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)Look at what changed. The process_payment dispatcher is four lines, and it will never grow. Want Apple Pay? Write a new function, slap @register("apple_pay") on it, put it in whatever file you like, and you're done. No central list to edit. No merge conflict. No reopening tested code. The handler sits right next to its own key, which is exactly where the next reader will look for it.
# Building a Reusable Registry Class
Once you have two or three registries lying around, you will get tired of rewriting the same decorator boilerplate. Wrap it in a small class and you get collision detection, better error messages, and a clean API for free:
class Registry:
"""A reusable name-to-object 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} already registered in {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()Now use it to build a text-processing pipeline driven entirely by config:
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())
# The pipeline is now just data. It could come from a YAML file,
# a CLI argument, or a database row.
pipeline = ["strip", "lowercase", "remove_digits"]
text = " Order #4521 CONFIRMED "
for step in pipeline:
text = transforms.get(step)(text)
print(repr(text))
Output:
'order # confirmed'This is where the pattern pays for itself. The behavior of the program is now described by data — a list of strings — not by code. Reordering the pipeline, adding a step, or handing the whole thing to a non-programmer through a config file all become trivial.
# Auto-Registering Classes With __init_subclass__
When your registry holds classes instead of functions, Python has an even slicker trick. The __init_subclass__ hook (available since Python 3.6) fires automatically every time a subclass is defined, so subclasses register themselves with no decorator at all:
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.parquetNo decorator anywhere. Subclassing DataLoader with a fmt= argument is enough to register the new class. This is how a lot of frameworks build their plugin systems under the hood.
# Where the Registry Pattern Is Useful in Practice
This is not an academic exercise. It is the backbone of tools you already use.
- Machine learning experiment configs. Hugging Face Transformers, Detectron2, and MMDetection all use registries so you can pick a model, optimizer, or augmentation by string name in a YAML file. build_model({"model": "resnet50"}) beats a giant if backbone == ... block, and it lets researchers add architectures without ever touching the trainer.
- File format and parser dispatch. Map extensions like "csv", "json", and "parquet" to loader classes. Supporting a new format becomes "write one class," not "edit the loader."
- Web framework routing. Flask's @app.route("/users") and Click's @cli.command() are registries in disguise. The decorator maps a URL or command name to the function that handles it.
- Plugin architectures. Any "drop a file in this folder and it just works" system — whether pytest fixtures, Airflow operators, or serializer backends — is almost always a registry collecting components at import time.
- Event handlers and state machines. Map event names or states to handler functions instead of branching on them. The transition table turns into a readable dictionary rather than a nest of conditionals.
# Practical Considerations and Things to Watch Out For
- Registration only happens on import. A decorator runs when Python executes the file it lives in. If your handlers sit in handlers/apple_pay.py and nothing ever imports that module, the @register decorator never fires and the handler quietly goes missing. The fix is to make sure registration modules get imported — usually through an explicit import in a package's __init__.py, or a small discovery loop with pkgutil.iter_modules that imports everything in a plugin folder.
- Guard against silent overwrites. With a plain dict, two components registering the same key clobber each other without a peep. As the Registry class above shows, raising on a duplicate key turns a baffling runtime bug into an obvious error at import time.
- Show people what is available. Always expose the keys with list(registry.keys()) and put them in your error messages. "Unknown model: 'lgbm'. Available: ['logreg', 'random_forest', 'xgboost']" saves far more debugging time than a bare KeyError.
- Do not reach for it too early. A registry is overkill for two or three stable branches whose logic genuinely differs. If the branches share no common signature, or the conditions are ranges rather than discrete keys (if score > 0.9 ... elif score > 0.5 ...), a plain conditional is clearer. The registry wins in one specific situation: you are dispatching on a discrete key to interchangeable behaviors, and you expect that set of behaviors to grow.
# Wrapping Up
The registry pattern trades a growing, central, hard-to-extend if/elif/else chain for a lookup table that components fill in themselves. The payoff is concrete. Your dispatcher stops changing. New features show up as new files instead of edits to old ones. Behavior becomes something you can drive from a config. And users of your code get a real extension point instead of a locked door.
Start small. Next time you catch yourself typing a third elif name == ..., stop and ask whether a dictionary would do. Usually it would. From there, the decorator and class versions are a short hop away.
# Before
if kind == "a": ...
elif kind == "b": ...
elif kind == "c": ...
# After
@registry.register("a")
def handle_a(): ...Your future self, scrolling past a four-line dispatcher instead of a 200-line ladder, will thank you.
Kanwal Mehreen** is a machine learning engineer and a technical writer with a profound passion for data science and the intersection of AI with medicine. She co-authored the ebook "Maximizing Productivity with ChatGPT". As a Google Generation Scholar 2022 for APAC, she champions diversity and academic excellence. She's also recognized as a Teradata Diversity in Tech Scholar, Mitacs Globalink Research Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having founded FEMCodes to empower women in STEM fields.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み