Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Singledispatch based on value instead of type

Tags:

python

django

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?

like image 470
Ivan Semochkin Avatar asked Apr 25 '16 09:04

Ivan Semochkin


2 Answers

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()
like image 157
Ilja Everilä Avatar answered Oct 13 '22 00:10

Ilja Everilä


Another way, if you want to use single dispatch, is to define state classes for states. You can make them "as string-like as possible". E.g. something like:

from functools import singledispatch

class State:
    @classmethod
    def make_state(cls, state_name: str) -> 'State':
        state = type(state_name, (State,), {})
        setattr(cls, state_name, state)
        return state()

    def __str__(self):
        return self.__class__.__name__


state_a = State.make_state("a")
state_b = State.make_state("b")

@singledispatch
def foo(s: State) -> str:
    raise NotImplementedError("Used dispatched")

@foo.register
def foo_a(s: State.a):
    return 'a'

@foo.register
def foo_b(s: State.b):
    return 'b'

print(foo(state_a))
print(foo(state_b))
# prints:
# a
# b
like image 39
shaunc Avatar answered Oct 13 '22 00:10

shaunc