Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python class method chaining

To avoid getting lost in architectural decisions, I'll ask this with an analogous example:

lets say I wanted a Python class pattern like this:

queue = TaskQueue(broker_conn)
queue.region("DFW").task(fn, "some arg") 

The question here is how do I get a design a class such that certain methods can be "chained" in this fashion.

task() would require access to the queue class instance attributes and the operations of task depends on the output of region().

I see SQLalchemy does this (see below) but am having difficulty digging through their code and isolating this pattern.

query = db.query(Task).filter(Task.objectid==10100) 
like image 415
NFicano Avatar asked Feb 14 '14 17:02

NFicano


People also ask

What is method chaining Why is it bad?

The drawback to self-referential method chaining is that you communicate that multiple method calls are required to do something, and that each call builds off the last. If this is not true, then method chaining could be communicating the wrong thing to other programmers.

Can append method be chained?

append is a bit of a special case for method chaining because it creates a new element and returns a selection to it. In most cases methods that operate on an object will return the same object being operated on.

What does __ call __ do in Python?

__call__ in Python The __call__ method enables Python programmers to write classes where the instances behave like functions and can be called like a function. When the instance is called as a function; if this method is defined, x(arg1, arg2, ...) is a shorthand for x.

What do you mean by method chaining?

Method chaining, also known as named parameter idiom, is a common syntax for invoking multiple method calls in object-oriented programming languages. Each method returns an object, allowing the calls to be chained together in a single statement without requiring variables to store the intermediate results.


2 Answers

SQLAlchemy produces a clone on such calls, see Generative._generate() method which simply returns a clone of the current object.

On each generative method call (such as .filter(), .orderby(), etc.) a new clone is returned, with a specific aspect altered (such as the query tree expanded, etc.).

SQLAlchemy uses a @_generative decorator to mark methods that must operate and return a clone here, swapping out self for the produced clone.

Using this pattern in your own code is quite simple:

from functools import wraps

class GenerativeBase(object):
    def _generate(self):
        s = self.__class__.__new__(self.__class__)
        s.__dict__ = self.__dict__.copy()
        return s

def _generative(func):
    @wraps(func)
    def decorator(self, *args, **kw):
        new_self = self._generate()
        func(new_self, *args, **kw)
        return new_self
    return decorator


class TaskQueue(GenerativeBase):
    @_generative
    def region(self, reg_id):
        self.reg_id = reg_id

    @_generative
    def task(self, callable, *args, **kw):
        self.tasks.append((callable, args, kw))

Each call to .region() or .task() will now produce a clone, which the decorated method updates by altering the state. The clone is then returned, leaving the original instance object unchanged.

like image 194
Martijn Pieters Avatar answered Oct 23 '22 12:10

Martijn Pieters


Just return the current object from region method, like this

def region(self, my_string):
    ...
    ...
    return self

Since region returns the current object which has the task function, the chaining is possible now.

Note:

As @chepner mentioned in the comments section, make sure that region makes changes to the object self.

like image 6
thefourtheye Avatar answered Oct 23 '22 10:10

thefourtheye