高校国語788336 views
いろは3010814 views
高校化学2924392 views
英語613639 views
ヒストリア290598 views
中学英語811710 views
数学講師2886227 views
LaTeX962153 views
教育149510 views
中学理科1630641 views
Help
Tools
NewsSpreadsheetCalendarBookkeepingSlidesTier ListPen ToolIllustrationCrayonWatercolorPixel ArtASCII ArtPerspectiveEndless StairsGraphMind MapER DiagramFamily TreeMemeCurved TextImage EditorMosaicRetro FilterPencil SketchSwirl EffectOCR/HighlighterMakeup EditorFaviconVideo TrimmerScrolling VideoVideo TitleColor PickerColor ExtractorBonfireFireworksWater RippleWater SplashBreaking GlassWood GrainMarble TextureCSS ButtonIcon MakerBar ChartGrouped Bar ChartStacked Bar ChartPie ChartLine ChartArea ChartStacked Area ChartScatter Plot3D Bar Chart3D Pie ChartBar Chart RaceBubble ChartPopulation PyramidPictogramEarningsCandlestick ChartInvestment RiskMortgage SimulatorCalculatorMatrix CalculatorFunction GraphPolynomial ExpansionVenn DiagramField VisualizerRubik's Cube Group TheoryTraveling SalesmanVoronoi and DelaunayFractalColumn ArithmeticDraw Math FiguresArithmetic AnimationArithmetic Word ProblemsCounting with Tree DiagramsCube NetsRolling DiceCross SectionsMotion PathMechanicsWavesElectromagnetic WavesLight and LensesThermodynamicsHow Semiconductors WorkMolecular StructuresAtomic OrbitalsElectrochemical CellsChemical EquilibriumCrystal LatticesBuffer pHPeriodic TableComplex IonsDNA Double HelixCell DivisionMembrane ChannelsNerve ImpulseMuscle ContractionHormones and HomeostasisRock ClassificationWeatherConstellationsSolar and Lunar Eclipses3D ModelingFloor PlanSeismic StructuresIntersection TurnMaglevCooking AnimationOrigamiLive Viewer CountGeoJSON MapRailway MapPopulation MapCrime MapLand Price MapSchool MapShrine and Castle MapHouse of Representatives MapWord MapSolitaireReversiHakoiri MusumeChessHamburgerRippleSlide Puzzle MakerNeon PinballNovel MakerJapanese Typing PracticePiano Score EditorMusic TheoryShogi StrategyPiano Rhythm Game

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