runtime
170 / 250
lesson 170 Functional & Dynamic Python · advanced

Registries: choose a function from data

A dictionary from names to functions replaces a growing if/elif chain: the program looks up which function to run from data, such as a file format or an event type. A registration decorator lets each function add itself, so new handlers don't require editing the dispatcher.

You'll be able to
  • Replace an if/elif chain on a name with a dictionary lookup, with a clear error for unknown names
  • Write a decorator factory that registers functions under a name and returns them unchanged
  • Decide what happens to duplicates and unknown names
The idea

An export function that handles CSV, JSON and Markdown usually starts as if fmt == "csv": ... elif fmt == "json": .... Every new format edits that chain, and the code for each format is buried inside it. A registry is a dictionary from a name to the function that handles it: the dispatcher becomes REGISTRY[fmt](data), each handler is a small function of its own, and the list of supported names is list(REGISTRY).

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Dispatch through a dictionary
import json

def to_csv(rows):
    header = ",".join(rows[0])
    lines = [",".join(str(row[key]) for key in rows[0]) for row in rows]
    return "\n".join([header] + lines)

def to_json(rows):
    return json.dumps(rows)

EXPORTERS = {"csv": to_csv, "json": to_json}

def export(rows, fmt):
    try:
        exporter = EXPORTERS[fmt]
    except KeyError:
        raise ValueError(f"unknown format {fmt!r}; choose from {', '.join(EXPORTERS)}") from None
    return exporter(rows)

rows = [{"code": "20621483", "sugars_100g": 13.2}, {"code": "01771879", "sugars_100g": 14.3}]
print(export(rows, "csv"))
print(export(rows, "json"))
try:
    export(rows, "xml")
except ValueError as err:
    print(err)

With many handlers, writing the dictionary by hand gets error-prone. A registration decorator (a factory, the Decorators & Context Managers module) lets each handler register itself where it is defined. The decorator stores the function and returns it unchanged, so it is still an ordinary function too.

example-2.py · Handlers that register themselves
HANDLERS = {}

def handles(event_type):
    def register(func):
        if event_type in HANDLERS:
            raise ValueError(f"{event_type} already handled by {HANDLERS[event_type].__name__}")
        HANDLERS[event_type] = func
        return func
    return register

@handles("WatchEvent")
def starred(event):
    return f"{event['actor']} starred {event['repo']}"

@handles("ForkEvent")
def forked(event):
    return f"{event['actor']} forked {event['repo']}"

def describe(event):
    handler = HANDLERS.get(event["type"])
    return handler(event) if handler else f"({event['type']} not shown)"

events = [
    {"type": "WatchEvent", "actor": "stackkoi785", "repo": "tidewater/smartqueue"},
    {"type": "PushEvent", "actor": "data_fox514", "repo": "cobalt-tools/deeplint"},
    {"type": "ForkEvent", "actor": "hack_lynx150", "repo": "tidewater/smartqueue"},
]
for event in events:
    print(describe(event))
print(sorted(HANDLERS), starred(events[0]) == describe(events[0]))

When to use it: formats, commands, event types, unit conversions, validation rules: anything chosen by a name that may grow. Mistakes to avoid: storing the result of calling the function (HANDLERS[t] = func()) instead of the function; forgetting return func in the registering decorator (the decorated name becomes None); letting a second registration silently overwrite the first; and a bare KeyError for an unknown name instead of a message listing the valid ones.

Review
  • A dictionary from names to functions replaces an if/elif chain; unknown names deserve a clear error.
  • A registration decorator stores the function and returns it unchanged; decide what duplicates do.
  • Store functions, not the results of calling them.
  • Next (see Algorithms & Complexity): measuring how much work an algorithm does as its input grows.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Registries: choose a function from data — Functional & Dynamic Python · Runtime