Real world use case example of simple python class constructor

What is a class and how it acts to create an instance?

All you need to know about Data Model on this matter and what is a class is well described in python documentation, and I guess that there is nothing to add to it:

Classes
    Classes are callable. These objects normally act as factories for new instances of themselves, but variations are possible for class types that override __new__(). The arguments of the call are passed to __new__() and, in the typical case, to __init__() to initialize the new instance.


object.__new__(cls[, ...])
    Called to create a new instance of class cls. __new__() is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument. The remaining arguments are those passed to the object constructor expression (the call to the class). The return value of __new__() should be the new object instance (usually an instance of cls).
    Typical implementations create a new instance of the class by invoking the superclass’s __new__() method using super().__new__(cls[, ...]) with appropriate arguments and then modifying the newly-created instance as necessary before returning it.|
    If __new__() returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to __new__().
    If __new__() does not return an instance of cls, then the new instance’s __init__() method will not be invoked.
    __new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.

python docs on Data model

Why to bother to use classes factories?

This is my real-life use case example how factories can improve code redability and provide more declaratice way of doing things. 

Recently I've created a simple workflow using repoze.workflow for my blog engine and ended up with code like this:

def simple_workflow():
    public = 'public'
    workflow = Workflow(
        state_attr='state',
        initial_state='private',
        name='simple_publication',
        permission_checker=None)
    permissions = {
        'viewing': ('view',),
        'managing': ('view', 'edit')}
    workflow.add_state(
        public,
        acl=(P(Allow, (Everyone, ), permissions['viewing']),
             P(Allow, prop('editors'), permissions['managing']),
             P(*DENY_ALL)))
    workflow.add_state(
        'private',
        acl=(P(Allow, prop('editors'), permissions['managing']),
             P(*DENY_ALL)))
    workflow.add_transition('publish', 'private', 'public')
    workflow.add_transition('hide', 'public', 'private')
    workflow.is_published = lambda context: getattr(context, workflow.state_attr, None) == public
    workflow.check()
    return workflow

As you can see imperative workflow declaration become a bit messy in my case, probably it because repozo.worlflow mostly targets a zml input format as way to declare workflow. Nevertheless repoze.workflow is pretty nice tool to use, I would say that it is the best choise in lightweight category, but there was couple extra things that I've needed from my workflow this time, which inludes:

  • registry for all available states, to be able to address each state as a property, kind of enum
  • ability to add some extra methods to check states of objects, etc, so workflow instance could be used more efficiently in views or templates  

As possible solution for that I've considered subclassin of repozo.worlflow, but it won't be good way to go, mostly because I've needed both - a way to declare states and custom methods/properties that depend on those states. As natural way to achieve that I've used a python class factory feature, in kind of same way how sqlachemy or other similar tools do it. In class constructor I declaratively described states, transactions, additional methods and properties, and make a class factory to build a workflow instance from it. So in the end my workflow configuration become like this:

class SimpleWorkflow(WorkflowBuilder):
    """ Simple workflow with only public/private states """

    name = 'simple_publication'
    state_attr = 'state'
    description = ''
    permission_checker = None

    permissions = {
        'viewing': ('view',),
        'managing': ('view', 'edit')}

    public = State(
        acl=(P(Allow, (Everyone, ), permissions['viewing']),
             P(Allow, prop('editors'), permissions['managing']),
             P(*DENY_ALL)))
    private = State(
        acl=(P(Allow, prop('editors'), permissions['managing']),
             P(*DENY_ALL)))

    initial_state = private

    publish = Transition(private, public)
    hide = Transition(public, private)

    def is_published(self, context):
        """ checks if context is published and returns bool result """
        return getattr(context, self.state_attr, None) == self.states.public

Huge improvement I would say! But everything has its cost. In this case all messy logic of workfow instance constuction was incapsulated in factory class:

class PropsProxy:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

class State(PropsProxy):
    pass

class Transition(PropsProxy):
    pass

class WorkflowBuilder:
    __baseclass = Workflow

    def __new__(cls):
        init_params = {n: getattr(cls, n) for n in signature(cls.__baseclass).parameters}
        states = {n: a for n, a in cls.__dict__.items() if isinstance(a, State)}
        transitions = {n: a for n, a in cls.__dict__.items() if isinstance(a, Transition)}
        members = {
            n: a for n, a in cls.__dict__.items()
            if not(n.startswith('_') or n in [*init_params, *states, *transitions])}
        members['states'] = type(
            cls.__name__ + 'States', (object, ), {i: i for i in states})
        reverse_states = {v: k for k, v in states.items()}
        init_params['initial_state'] = reverse_states[init_params['initial_state']]

        workflow_cls = type(cls.__name__, (cls.__baseclass, ), members)

        workflow = workflow_cls(**init_params)

        for name, attr in states.items():
            workflow.add_state(name, *attr.args, **attr.kwargs)

        for name, attr in transitions.items():
            args = [reverse_states[i] if isinstance(i, State) else i for i in attr.args]
            kwargs = {
                k: reverse_states[v] if isinstance(v, State) else v 
                for k, v in attr.kwargs.items()}
            workflow.add_transition(name, *args, **kwargs)

        workflow.check()
        return workflow

I guess that this under the hood workflow instances constructor looks even messier than my original worflow consttuctoin function, but in comparing it has huge improvment of worflow declaration, including:

  • makes workflow configuration declarative
  • workflow configuration declaration becomes lintable
  • workflow configuration is extecsible, adding new method properties is easy

Afterword

I hope this small example will engourage people to use factories, but that is not the best guide on how to make thouse fatories. 

Prev Post Next Post