12

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) 
NFicano
  • 1,005
  • 10
  • 26

2 Answers2

14

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.

Martijn Pieters
  • 889,049
  • 245
  • 3,507
  • 2,997
  • Actually I think the solution is what I'm looking for, but having difficulty implementing it: http://pastebin.com/HpsgK0T5 - I been trying to debug it, but have been unsuccessful, 'TaskQueue' object is not callable – NFicano Feb 14 '14 at 20:44
  • @NFicano: you changed the signature of the decorator; you replaced `self` with `fn`. – Martijn Pieters Feb 14 '14 at 20:47
  • @NFicano: I see now that I did make a typo in my answer; corrected `fn(...)` to `func(...)`. – Martijn Pieters Feb 14 '14 at 20:48
  • 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! – NFicano Feb 14 '14 at 20:54
  • @MartijnPieters s = `self.__class__.__new__(self.__class__)` what does this line mean? does `self.__class__` refer to the child or base class? and what does `self.__new__` mean? – Halcyon Abraham Ramirez Jul 31 '15 at 14:54
  • @HalcyonAbrahamRamirez: see [`object.__new__()`](https://docs.python.org/2/reference/datamodel.html#object.__new__); `instance.__class__` is the class that produced the instance. It produces a *new* instance of the same class, whatever 'the same class' means for the current instance. – Martijn Pieters Jul 31 '15 at 15:19
  • so `instance.__class__` basically refers to the name of the class where `instance.__class__` is called? so in your `class TaskQueue` since you decorated `region` and `.__class__` is called in the `@_generative` decorator so `.__class__` is `TaskQueue`? which is the name of the class – Halcyon Abraham Ramirez Jul 31 '15 at 15:35
  • 1
    @HalcyonAbrahamRamirez: no, it is the actual class object. The same object that `type(instance)` returns. – Martijn Pieters Jul 31 '15 at 15:38
  • @MartijnPieters I tested .__class__ in IDLE and it refers to the class name? for example lets say `class Num` has a class attribute called `digit` and is = 5 so that's `digit = 5` I tried `print(a.__class__.page)` outputs `5` so it is "basically" calling the class name. like how you would call a class attribute? – Halcyon Abraham Ramirez Jul 31 '15 at 16:09
  • @HalcyonAbrahamRamirez: this is going way beyond what comments are usable for. Can you please use chat for this? There is a [Python chat room](https://chat.stackoverflow.com/rooms/6/python) for example. – Martijn Pieters Jul 31 '15 at 16:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/84828/discussion-between-halcyon-abraham-ramirez-and-martijn-pieters). – Halcyon Abraham Ramirez Jul 31 '15 at 16:40
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.

Community
  • 1
  • 1
thefourtheye
  • 206,604
  • 43
  • 412
  • 459
  • 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). – chepner Feb 14 '14 at 17:28
  • You'll need to return a *clone* here; one that is a copy of `self` but with the changes applied. – Martijn Pieters Feb 14 '14 at 17:29
  • @MartijnPieters Why so? Could you please explain? – thefourtheye Feb 14 '14 at 17:30
  • @thefourtheye: That's what SQLAlchemy does: return you a new instance object that then allows further filtering. The original query object is not affected. – Martijn Pieters Feb 14 '14 at 17:32
  • @thefourtheye how exactly do we modify self? – Halcyon Abraham Ramirez Aug 01 '15 at 04:17