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.
- 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
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.
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.
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.
- 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.