Hacker News new | past | comments | ask | show | jobs | submit login
The Python dictionary dispatch pattern (jamesg.blog)
45 points by zerojames on Aug 29, 2023 | hide | past | favorite | 56 comments



Dictionary lookup is often a handy way to solve small tasks, when (a) there's no need to build a whole class hierarchy, etc. and (b) the solution is nicely declarative/data-oriented.

The article talks about string keys, but Python allows many things to be used as keys. For example, instead of chains of `elif` we can look up `True` in a dictionary:

  >>> age = 25
  >>> {
  ...   (age < 18): "Forbidden",
  ...   (age == 18): "OK, you're old enough now",
  ...   (age > 18 and age < 20): "LOL teen emoji amirite?",
  ...   (age >= 20 and age < 40): "Hi",
  ...   (age >= 40): "Welcome",
  ... }[True]
  'Hi'
One thing that's annoying with this pattern is that Python calculates values eagerly, so we can't use it for expensive calculations or observable effects. To make the values lazy we can wrap them in `lambda:`, but at that point we're essentially re-implementing dynamic dispatch, so classes may be better (or a couple of `elif`s, at most). Note: that's what the article's doing ;)


There is a problem with "overlapping" conditions. If multiple conditions in a elif statement would evaluate to true, then the first of these conditions is chosen. For this dictionary approach, the last occurrence of `True` as a key is chosen.

The two are not so easily translatable I guess, considering further conditions usually have the built-in assumption that all preceding conditions were evaluated to false.


That's neat but it's also kinda the definition of "clever code". When a later reader is going to have to pause and ask themselves what on earth is this.


Honestly when I accepted that clever code is bad code, my passion for coding died and now I just want to coast until I get fired and then go work at a phone repair shop

I think when I picked my major, I never understood what software engineering is, or even what engineering is. Honestly I shouldn't be complaining. So many people have it worse.

Coding would still be fun if it weren't a job, where we bend tech backwards to please the users (instead of teaching users to do the right thing)


There are still places you could clever code your way around problems. But production code should still be free of clever code.


It's worse than that because anyone who's touched a Lisp will think "this is a cond system! I know this!" and get the semantics exactly wrong.


> (age > 18 and age < 20): "LOL teen emoji amirite?",

In Python you can write this more concisely as:

(18 < age < 20)


btw, Python has had pattern matching for almost two years now: https://peps.python.org/pep-0622/

If you support Python 3.10+ you can do this far more cleanly.


True, although pattern-matching is unfortunately based on statements, which makes it less useful than if it were an expression (e.g. it can't be used in 'lambda', can't be used in function arguments, can't be used in a dictionary definition, can't be assigned to a variable, can't be used in generator/comprehension, etc.).

Also, Python requires explicit returns (unless we're in a lambda), so using a `match` statement for the above example would need multiple duplicate statements (either `return`, or assignment to a repeated variable name, or calling a repeated continuation (although that's less common, due to Python not eliminating tail calls), etc.). A chain of `elif` would also have that problem.


Is this any more clear than an elif expression?

I suppose the condition coming first is nice


Personally I find this a whole lot clearer. It's a table lookup, instead of a series of sequential checks (especially if you listen to your linter and have the elif version with the conditions and values on alternating lines, vs the conditions together in a vertical column like this).


Not sure if this was the intent, but to anyone reading this....this dict doesn't have the perf benefits of a "table lookup"; in fact it's slower.


The question was about readability, not performance.


I've found that lots of dictionary dispatch in Python ends up feeling a lot nicer as class attributes. You get nicer typing properties, autocomplete, don't need to type quotation marks in many places, and you don't end up in the weird "use lambdas or define functions and then refer to those in the dictionary".

Of course if you're trying to do a registry-pattern style thing the constraints are different (though there I also think that you end up wanting to base your stuff on classes whose metaclass registers itself based on a string-y class attr or something...)

Though in the end it's super dependent on what kind of code you're trying to write. No rule is absolute, especially in this category of aesthetics.


If you're hardcoding it sure, but at that point you should consider just using a module with bare functions. This pattern is for when the lookup value comes from somewhere else, and to use it with attributes requires a more awkward getattr() call, and it has a less obvious error message if the value isn't in the lookup.


Terrible example, because he is writing the keys when calling the functions:

  mydict[“foobar”]()
is basically a worse version of using a module

  mymod.foobar()
The REAL value of this pattern is when the keys are going to change on runtime:

  for name in userfilters:
    im = filtersdict[name](im)


Even when changes are going on at runtime, strings are rarely the right answer. There's a name for when this approach is taken so far as to become an antipattern: stringly typed programming. [0]

Typically, enums should be used in place of the way strings are used in the post. The obvious advantages of enums: they use language-level identifiers, so the language is able to check whether you're referring to something that exists. IDE autocompletion will work properly, so you won't have to memorise which strings are accepted where. Enums can also nicely leverage a static type system in languages that support it (i.e. it should be a compile-time error to try to use a member value from an irrelevant enum). Dispatching over an enum may also have performance benefits, if that matters.

That said the post does mention that the strings-as-keys approach is particularly useful in hand-written parsers, which sounds like a reasonable exception.

[0] https://blog.codinghorror.com/new-programming-jargon/


Even when key change at runtime, wouldn't you use getattr on a module?

  for name in userfilters:
    filter_function = getattr(mymod, name, None)
    if filter_function is None:
        raise NotImplementedError(name)
    im = filter_function(im)


What is value are you envisioning is associated with the dynamic `name` key in your example?

In my mind, this is more a case where I might return an anonymous function that captures the name, but it's less clear to me what the dict is for.


> The REAL value of this pattern is when the keys are going to change on runtime

Which is demonstrated in the article, see second example (third and fourth code blocks).


Love this! Thank you for sharing (I'm the author).


I prefer doing this directly in a registry/factory class, eg:

    class Registry:
      constructors = {}

      @classmethod
      def register(cls, other):
        # you can do it by an implicit property or the class name in simple cases. It's better not to use implementation details here, eg you should use an explicit class attribute 
        # you can also verify that the other class holds certain invariants here, so it fails immediately 
        cls.constructors[other.__name__] = other
        # cls.contructors[other.name] = other

      @classmethod
      def from_name(cls, name):
        # error handling code here
        return cls.constructors[name]


    # in something.py
    @Registry.register
    class Something:
      def __init__(self, foo, bar=None):
        self.foo = foo
        self.bar = None

    # In something_test.py
    import something
    import registry

    smth = registry.from_name("Something")

    s = smth('foo', bar=2)


You'll want to have a registry for each base class (they largely all look the same except for the invariant checks), but this way you don't have to update a map, you just add the decorator to new classes as you build them. It also allows classes to be completely unaware of the registry and focus on their actual task.

Additionally, the invariant checking can do all sorts of things that you can't do with a base class, eg making sure that overridden properties are still properties, checking that class attributes follow whatever formatting convention you have, etc etc


I don't understand what value the dictionary is providing here, I suspect none?

How is this better than just functions in a module? (import the module as math_exprs, use getattr)


this. At least with the current example and even the surrounding chatter in this thread, I don’t see any benefit and plenty of room for downsides. Some of which haven’t even been mentioned yet (e.g how this will play with IDEs)

I don’t mean to poo poo on it. I really do like learning new patterns, but I haven’t yet grasped this one’s utility.


You could modify it easily, for example the user could register an additional function to the mapping. That would not be as easy to do with a module and getattr().

You can also list the registered function with math_exprs.keys() and that might not be as easy with a module either.


if all the keys are unknown until runtime then I guess the dict makes sense

I don't think it makes much sense in the example code given


You use it to dispatch to the appropriate function in a module based on the value of a runtime variable


It's a favourite pattern of mine too.

I'm pretty sure it's bad practice but in one project i needed some form of parameterised extensibility (parsing a broad mix of files onto a standard format). So what i did was have other devs subclass an abstract base class I created (with some predefined attributes and methods to fill in) and then generated my execution dictionary based on the attributes of the subclasses of the base class.


This doesn't sound like a bad pattern to me. (Though I certainly think I have seen bad implementations of it.)

But if the keys are known statically (as in the article), I think it's generally better to use named functions.

But it's good to be aware of the capability, for cases like you're describing.


I sometimes use this pattern in combination with a decorator to register functions which I later dispatch to dynamically. Something like:

  funcs = {}

  def register(key):
    def decorator(func):
      funcs[key] = func
      return func
    return decorator


  @register("some_data_key")
  def do_something_with_some_value(value):
    return value

  # later....
  
   def parse(data):
     return {
       key: func(data.get(key)) for key, func in funcs.items()
     }


There's an equivalent JS pattern that I always suggest over using switch cases, as it's far more idiomatic:

  const case = 'case1'

  const result = {
    'case1': 'value1',
    'case2': 'value2',
    'case3': 'value3',
    'case4': 'value4',
  } [case] || 'defaultVal'


I use this pattern a lot. It's also nice to be able to do dict.get(key, lambda: raise ValueError(f'no handler for {key}')) to have a nice fallback or raise in a convenient one liner.


That's not at all valid syntax, this does not work.


yeah sorry the 'raise' is not valid in-line I did this from mobile - normally I would define the fallback handler elsewhere and/or place it in the map under a key like 'default'


Anything that breaks IDE type hinting and usage and definition linking is a non-starter for me unless the entire scope of the function fits in about a dozen or so lines.


Despite the danger of being considered too cynical...

"When your OOP support sucks so bad, the people might as well just use maps."


I can at best see this pattern as useful for some select use cases. At least, that’s when I’ve used it myself. Python’s class system generally works completely fine for me, as someone that’s been using it for the better part of a decade full-time.


Not even, I believe objects in python are just maps with some syntax sugar. I’m not sure what this is buying over a static class aside from being slower and more awkward to use?


It's open so you can change the dispatch during runtime. It's also pretty nice to dispatch on strings as not all situations require complicated setup with class hierarchies, enumeration classes, or what have you.


Documentation, type hints and arguably readability.

But I understand that this is a very python answer.


> I’m not sure what this is buying over a static class

1. It is not static. It can be configured, changed at runtime, etc

2. First order functions are very powerful as well


Modifiability is also true of python objects and classes. This is basically reinventing obj.__dict__

The real difference is that since it's not an object, "self" isn't in the function signature which to be fair can be annoying for a collection of stateless functions. So this is actually closer to a module.


Yeah, you alluded to this at the end, but you don't need to define these as methods on an instance (requiring `self`) if they don't require state.


To be fair, I have come prefer approaching the problem stated in the link in the exact opposite direction. Specifically the part about having a collection of functions with signatures that are similar/identical. The example in the post is of a simple signature, but if you have more complex (and long) signatures and a bunch of functions that use them, it might be worth thinking about replacing the argument list with a frozen dataclass.

    @dataclass(frozen=True)
    class MyArguments:
        a: int,
        b: str,
        c: list = None

    def func1(self):
        ...

    def func2(self):
        ...
etc. You can also sprinkle in @property and functools things like @cached, @cached_property when desired. It also lets you create functions that transform MyArguments into different MyArguments.


Yeah I'm with you here too.


The struggle to figure out what the hell to name MyArguments is real. But that's also true about picking a name for the dict-of-functions-that-have-a-similar-weird-signature. :D


(1) seems like a bug rather than a feature, to me, outside extremely narrow use cases. It's generally much better to be able to statically reason about what code exists and how it is structured and called, than to have to reason about runtime behavior to determine those things.

I agree with (2), but I'm not sure the pattern from the OP is a good example of using that power. It seems like using a language feature - mapping a string to an anonymous function - when a better one exists: defining named functions. Named functions can be looked up dynamically by name in python, and are (I think) generally more clear to a reader, especially with the standard documentation and type hints.


1) is absolutely not a bug, though sure you don't want to overuse it"

"Oh but you can replace this with a derived class", it's not as flexible (for example, mix and matching, etc)

> than to have to reason about runtime behavior to determine those things.

Sure if you only do CRUD you probably won't run into those cases. Math heavy, scientific code, simulations, etc might disagree


> "Oh but you can replace this with a derived class", it's not as flexible (for example, mix and matching, etc)

What do you mean by "mix and matching"? I feel like a concrete example might help me understand what you're thinking of in this thread.

> Sure if you only do CRUD you probably won't run into those cases. Math heavy, scientific code, simulations, etc might disagree

Ha, it's funny because I've recently started doing more math-y / scientific / simulation work in python, after a career spent mostly doing CRUD-y things in not-python, and I've never been more convinced that it is best to make code as statically legible as possible. It's hard enough to make sure the math and the algorithms are right, I don't want to also be reasoning through dynamic abstractions.

But again, maybe there's a concrete thing you're thinking of here that I'm not seeing!


> What do you mean by "mix and matching"?

A bit of a contrived example, but think of multiple inheritance but dynamically.

> maybe there's a concrete thing you're thinking of here

Genetic programming would be the main use case of this (of table dispatch, not the "mix and match thing", which ok, is a bit of a corner case

> I've never been more convinced that it is best to make code as statically legible as possible

Given how the average academic code is, I don't think I can disagree with you much

And I think it's also a question of syntax. For those kinds of things a lambda might be more readable, but a typed lambda is still a lambda.


In the article he describes using this for user-provided mathematical expressions. I suppose that makes sense if you're implementing a light DSL in python... But maybe don't?

Edit: I don't believe this is any slower, actually. It may even be marginally faster than a class, since a class adds some QOL features that will add a little overhead that would add a few branches to the code path.

But if you're worried about speed, don't use Python anyways.


why not just

  SUPPORTED_INFERENCE_MODELS = {
    "groundingdino": registry.grounding_dino_base,
    "yolov8": registry.yolov8_bas,
    ...
  }


I'd be less than enthuisiastic about relying on a function named "yolo<something>". I take "You Only Live Once" (YOLO) to express approval of risk-taking, which isn't usually my intent when I rely on an API.


YOLO is a model architecture in computer vision (You Only Look Once).

https://blog.roboflow.com/whats-new-in-yolov8/

In one example, dictionary dispatch decides what model to use: YOLOv5, v8, SAM, and others.


Use this to build an affective programming interface: call "yolo", "y̴o̸l̶o̸", "ÿ̵͔̽̑̀̋͋̒͠o̷̢̫͉̠̭̭̔͗̑̎̍l̴̡̡̨̹͈̤̯̜̓̅͂̓̽͊͂̔o̴̭̙̼̺̹̭͚̊̔͑̽͆͝" or "y̸̛̬̣̯͇̆͑̂̍̈́̏́̆̒̌͆̐̉̑͒̆̐̄̄̒͒͘͝͝͝͝͝ơ̴̡̧̧̛̥͖͖͕̪̣̜̮̜͉̹̙̠̬̝̱̹͇̻͖̲̦̲͚̣̦̏̿͑͌̽̒̅̏̈̉̃͆̾̌̽̽̐̆̃̇͋̐͂̏̓͛̈̈́͊̆̔̒͂͂͛̃͂̀̽͛̾̎͋̏̑̀̌͑̏̒́̆͗́͊͂͂͑̎͘͘͝͠͠ͅͅl̵̨̨̧̛̺̖̫͇̬̻͕̠̬̮͎̖͎̟̱̟͙͉͓͔͖͕̗̜̝͚̠͔̹͎̭̻͙̭͚͎̝͍̯̭̖̠̳̱̩̱̠̜̯̱̠̻̯̻͚̙͖̭̟̹͆͆̀̆͌̓̇̍̇̏͂̽̇̇͛̓͘͜͜͝ͅͅͅỏ̷̢̹͎̺̞͚̥̲̐͋̈́̑̈̈͘̚" to set the tone appropriately.


pretty sure yolo is an object recognition ML model




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: