中学理科1626207 views
いろは2986023 views
LaTeX957300 views
高校生物549842 views
ヒストリア284143 views
世界の国560595 views
Computer365120 views
中学数学621382 views
りんご192546 views
高校化学2913383 views
Help
Tools

English

条件分岐を減らすためのディスパッチテーブル設計(Python)

多数の 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 を整理し、保守性の高いコードを実現できる。