13

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) 

2 Answers 2

17

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.

Sign up to request clarification or add additional context in comments.

11 Comments

Actually I think the solution is what I'm looking for, but having difficulty implementing it: pastebin.com/HpsgK0T5 - I been trying to debug it, but have been unsuccessful, 'TaskQueue' object is not callable
@NFicano: you changed the signature of the decorator; you replaced self with fn.
@NFicano: I see now that I did make a typo in my answer; corrected fn(...) to func(...).
AH no I didn't see your edit, I knew fn wasn't right, but couldn't figure out what it was suppose to refer to, thanks again!
@HalcyonAbrahamRamirez: no, it is the actual class object. The same object that type(instance) returns.
|
6

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.

5 Comments

Also, region needs to modify self in some way, since presumably queue.region("DFW").task(fn, "some arg") should be different from queue.task(fn, "some arg"). Or, region() should return not self, but some other TaskQueue object (or, whatever object has a task method).
You'll need to return a clone here; one that is a copy of self but with the changes applied.
@MartijnPieters Why so? Could you please explain?
@thefourtheye: That's what SQLAlchemy does: return you a new instance object that then allows further filtering. The original query object is not affected.
@thefourtheye how exactly do we modify self?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.