pythondjango

Singledispatch based on value instead of type


I building SPA on Django and I have one huge function with many if statement for checking state name of my object field. Like this:

if self.state == 'new':
    do some logic
if self.state == 'archive':
    do some logic

and so on. I reading nice book "Fluent python" now, and I mention about @singledispatch decorator, it looks so great, but it can overide function only with diferent type of parametres like str, int, etc.
Question is, if there in python or Django way to separate logic like in my huge function with overided function like singledispatch do?


Solution

  • There is, though you have to write it. One possibility is to create a descriptor that does the dispatching based on instance.state or any chosen state_attr:

    class StateDispatcher(object):
    
        def __init__(self, state_attr='state'):
            self.registry = {}
            self._state_attr = state_attr
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
    
            method = self.registry[getattr(instance, self._state_attr)]
            return method.__get__(instance, owner)
    
        def register(self, state):
            def decorator(method):
                self.registry[state] = method
                return method
    
            return decorator
    

    https://docs.python.org/3/howto/descriptor.html#functions-and-methods:

    To support method calls, functions include the __get__() method for binding methods during attribute access. This means that all functions are non-data descriptors which return bound or unbound methods depending whether they are invoked from an object or a class.

    In your stateful class you can then create a dispatcher and register methods:

    class StateMachine(object):
    
        dispatcher = StateDispatcher()
        state = None
    
        @dispatcher.register('test')
        def test(self):
            print('Hello, World!', self.state)
    
        @dispatcher.register('working')
        def do_work(self):
            print('Working hard, or hardly working?', self.state)
    

    Let's see it in action:

    >>> sm = StateMachine()
    >>> sm.state = 'test'
    >>> sm.dispatcher()
    Hello, World! test
    >>> sm.state = 'working'
    >>> sm.dispatcher()
    Working hard, or hardly working? working
    >>> sm.state = None
    >>> sm.dispatcher()
    Traceback (most recent call last):
      ...
      File "dispatcher.py", line 11, in __get__
        method = self.registry[getattr(instance, self._state_attr)]
    KeyError: None
    

    Note that this is a quite evil method of dispatching based on state, since for future readers of your code the whole mechanism will be hard to follow.

    Another method of dispatching on textual state is to encode the state in your method names and choose the correct method based on that in a dispatching function. Many python classes use this pattern (ast.NodeVisitor for example):

    class StateMachine(object):
    
        def dispatch(self, *args, **kwgs):
            getattr(self, 'do_{}'.format(self.state))(*args, **kwgs)
    
        def do_new(self):
            print('new')
    
        def do_archive(self):
            print('archive')
    
    
    sm = StateMachine()
    sm.state = 'new'
    sm.dispatch()
    sm.state = 'archive'
    sm.dispatch()