多数の if-elif-else が連なるコードは、読みにくく保守も大変だ。ディスパッチテーブル(dispatch table)を使えば、条件分岐を辞書のキー検索に置き換えて、コードをシンプルにできる。この記事では、ディスパッチテーブルの設計パターンと実践的な使い方を解説する。
問題:if-elif の連鎖
典型的な例として、コマンド文字列に応じて異なる処理を行うコードを考える。
# if-elif の連鎖(読みにくい)
def handle_command(command, data):
if command == "create":
return create_item(data)
elif command == "read":
return read_item(data)
elif command == "update":
return update_item(data)
elif command == "delete":
return delete_item(data)
elif command == "list":
return list_items(data)
elif command == "search":
return search_items(data)
else:
raise ValueError(f"Unknown command: {command}")
コマンドが増えるたびに elif を追加していくと、関数が肥大化する。また、すべての条件を順番に評価するため、最悪ケースでは効率も悪い。
解決策:ディスパッチテーブル
辞書を使ってコマンドと関数を対応付ければ、条件分岐をキー検索に置き換えられる。
# ディスパッチテーブル
COMMAND_HANDLERS = {
"create": create_item,
"read": read_item,
"update": update_item,
"delete": delete_item,
"list": list_items,
"search": search_items,
}
def handle_command(command, data):
handler = COMMAND_HANDLERS.get(command)
if handler is None:
raise ValueError(f"Unknown command: {command}")
return handler(data)
辞書のキー検索は平均 O(1) で、コマンド数が増えても性能が劣化しない。また、ハンドラーの追加は辞書に 1 行追加するだけで済む。
パターン 1:単純なキー → 関数マッピング
最もシンプルなパターンは、キーと関数を 1 対 1 で対応させるものだ。
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Division by zero")
return a / b
OPERATIONS = {
"+": add,
"-": subtract,
"*": multiply,
"/": divide,
}
def calculate(a, op, b):
operation = OPERATIONS.get(op)
if operation is None:
raise ValueError(f"Unknown operator: {op}")
return operation(a, b)
# 使用例
print(calculate(10, "+", 5)) # 15
print(calculate(10, "*", 3)) # 30
パターン 2:ラムダ式を使ったインラインテーブル
処理が単純な場合、ラムダ式でインライン定義できる。
OPERATIONS = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b if b != 0 else float('inf'),
"**": lambda a, b: a ** b,
"%": lambda a, b: a % b,
}
result = OPERATIONS["**"](2, 10) # 1024
ただし、複雑なロジックをラムダに詰め込むと可読性が下がる。その場合は名前付き関数を使う方がよい。
パターン 3:クラスメソッドへのディスパッチ
クラス内でメソッドにディスパッチする場合、getattr を使うパターンもある。
class CommandProcessor:
def do_create(self, data):
return f"Created: {data}"
def do_read(self, data):
return f"Read: {data}"
def do_update(self, data):
return f"Updated: {data}"
def do_delete(self, data):
return f"Deleted: {data}"
def process(self, command, data):
method_name = f"do_{command}"
method = getattr(self, method_name, None)
if method is None:
raise ValueError(f"Unknown command: {command}")
return method(data)
processor = CommandProcessor()
print(processor.process("create", "item1")) # Created: item1
このパターンは、コマンドとメソッド名の命名規則を統一できる場合に便利だ。
パターン 4:デコレータで自動登録
デコレータを使うと、ハンドラーの登録を自動化できる。
HANDLERS = {}
def register(command):
def decorator(func):
HANDLERS[command] = func
return func
return decorator
@register("create")
def handle_create(data):
return f"Created: {data}"
@register("read")
def handle_read(data):
return f"Read: {data}"
@register("update")
def handle_update(data):
return f"Updated: {data}"
def dispatch(command, data):
handler = HANDLERS.get(command)
if handler is None:
raise ValueError(f"Unknown command: {command}")
return handler(data)
# 登録されたハンドラーを確認
print(HANDLERS.keys()) # dict_keys(['create', 'read', 'update'])
ハンドラーの定義場所が分散しても、デコレータが登録を担保するため、テーブルの更新忘れを防げる。
パターン 5:デフォルトハンドラーの設定
未知のキーに対するデフォルト処理を設定するパターン。
def default_handler(data):
return f"Default processing: {data}"
HANDLERS = {
"special": lambda data: f"Special: {data}",
"vip": lambda data: f"VIP: {data}",
}
def dispatch(key, data):
handler = HANDLERS.get(key, default_handler)
return handler(data)
print(dispatch("special", "x")) # Special: x
print(dispatch("unknown", "y")) # Default processing: y
パターン 6:複数条件の組み合わせ
タプルをキーにすれば、複数条件の組み合わせもテーブル化できる。
def process_new_free(user):
return "Welcome! Here's a free trial."
def process_new_paid(user):
return "Thank you for subscribing!"
def process_existing_free(user):
return "Upgrade to premium?"
def process_existing_paid(user):
return "Thank you for your continued support!"
HANDLERS = {
("new", "free"): process_new_free,
("new", "paid"): process_new_paid,
("existing", "free"): process_existing_free,
("existing", "paid"): process_existing_paid,
}
def greet_user(user_type, plan_type, user):
handler = HANDLERS.get((user_type, plan_type))
if handler is None:
return "Welcome!"
return handler(user)
print(greet_user("new", "paid", {"name": "Alice"}))
# Thank you for subscribing!
ディスパッチテーブルの利点と注意点
計算量が O(1) で高速。コードが宣言的で見通しがよい。ハンドラーの追加・削除が容易。テスト時にテーブルを差し替えられる。
複雑な条件(範囲チェック、複合条件)には向かない。デバッグ時にスタックトレースが読みにくくなることがある。
範囲ベースの条件には向かない
ディスパッチテーブルは「完全一致」の検索に適している。範囲ベースの条件分岐には bisect モジュールなど別のアプローチが必要だ。
# ディスパッチテーブルでは表現しにくい
# score に応じてグレードを返す
def get_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "D"
# bisect を使った代替
import bisect
def get_grade_bisect(score):
breakpoints = [70, 80, 90]
grades = ["D", "C", "B", "A"]
return grades[bisect.bisect(breakpoints, score)]
まとめ
ディスパッチテーブルは、離散的なキーに基づく条件分岐を整理する強力なパターンだ。
コマンド処理、イベントハンドリング、ステートマシン、プラグインシステムなど、キーと処理が 1 対 1 で対応する場合。
範囲ベースの条件分岐、複雑な論理条件、副作用の順序が重要な処理。
適切に使えば、肥大化した if-elif を整理し、保守性の高いコードを実現できる。