4

I am experimenting with fluent interfaces in Python.

An example of a fluent sql query generator would look something like this in usage:

sql.select('foo').select('bar').from('sometable').tostring() 

I quickly realized that the ability to define nested classes recursively would probably help.

class sql:
    class select:        
        class select   # <--  HERE
        def __init__(self, dbcolumn, astype=None, asname=None):
            self.dbcolumn = dbcolumn
            self.astype = astype
            self.asname = asname

In the line marked with the comment '# <-- HERE':
I want this nested class reference to refer to the same 'select' class definition of the containing class.

Is this possible somehow? Maybe using some keyword that I am not aware of?

Ries
  • 2,844
  • 4
  • 32
  • 45
  • 1
    But `select` is a *method* on `sql`; it could return a class, but it is not itself a class.. – Martijn Pieters Jul 25 '12 at 08:33
  • 6
    That's... not how you implement a fluent interface. – Ignacio Vazquez-Abrams Jul 25 '12 at 08:34
  • Would it be fair to say that fluent interfaces are just not the way things are done in Python? – Chris Morgan Jul 25 '12 at 08:38
  • @ChrisMorgan: Nope, you just don't nest class definitions to implement fluent interfaces. – Martijn Pieters Jul 25 '12 at 08:43
  • @MartijnPieters: select is *not* a method, just a nested class. Calling sql.select('foo') obviously calls the constructor of the sql.select class, returning an instance of the nested select class. – Ries Jul 25 '12 at 09:25
  • @Ries: but that would leave you no context of the parent class. There is no point in nesting classes that way, other than to confuse users of your API. It *should* be a method; your `sql.select` example told me you expected it to work like a method would. – Martijn Pieters Jul 25 '12 at 09:27
  • Its still a work in progress. Here is a gist showing how to pass the context along: https://gist.github.com/3175416 – Ries Jul 25 '12 at 10:09
  • @MartijnPieters: I know that you wouldn't do it *that* way, but I mean that as a whole, designing fluent interfaces without good reason is something that would not be considered Pythonic. It's not typical to get jQuery-style chaining, for example. – Chris Morgan Jul 25 '12 at 13:55
  • @ChrisMorgan: As SQLAlchemy shows, there are fine use-cases for chaining outside of jQuery, implementable in Python. I wouldn't say that this is thus un-pythonic. – Martijn Pieters Jul 25 '12 at 13:57

3 Answers3

7

There is no need for "recursive class definitions". All you need to do to allow chaining is to return self in your methods (or a instance of the same class if your objects are not mutable or some methods shouldn't modify the input object).

Example:

>>> class SQL(object):
...     def __init__(self):
...         self.columns = []
...     def select(self, col):
...         self.columns.append(col)
...         return self
...
>>> s = SQL()
>>> s.select('foo').select('bar').columns
['foo', 'bar']
ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • My (misplaced?) concern with this example is that adding a SQL.insert function would make something like this possible: s.select('foo').insert() which doesnt make sense in a SQL fluent interface. – Ries Jul 25 '12 at 09:12
  • 1
    Simply set a flag when `select` is called and raise an exception when `insert` is called on an object with the `select` flag set. – ThiefMaster Jul 25 '12 at 09:14
  • That sounds like an unnecessary hack. – Ries Jul 25 '12 at 09:26
  • If python had better code-completion compatibility the suggested flag would also not have played nice. But that is just a theoretical drawback. – Ries Jul 25 '12 at 09:29
0

You are confusing classes with methods.

select is a method on an instance of class sql, not a nested class. It could return another instance.

class SelectStatement(object):
    def select(self, tablename):
        return SelectStatement()

class SQL(object):
    def select(self, tablename):
        return SelectStatement()

Take a look at the source code of SQLAlchemy; it does exactly that; generate SQL from a structure python classes, where you can refine a query by calling methods on instances in just such a manner. A series of joins for example, can be chained:

q = session.query(User).join(User.orders).join(Order.items).join(Item.keywords)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • No confusion here. Maybe my use of lower case class names confused you? :) – Ries Jul 25 '12 at 09:16
  • @Ries: nope, I am pretty certain I was not confused there. – Martijn Pieters Jul 25 '12 at 09:18
  • Thanks for the link to SQLAlchemy anyway. – Ries Jul 25 '12 at 09:22
  • @Ries: You want to return class instances based on the context of another instance. You do that via methods; if you just instantiated a nested class it would not inherit that context. – Martijn Pieters Jul 25 '12 at 09:25
  • See https://gist.github.com/3175416 for a way how I could pass the context along. But I concur, it uses a method that instantiates the same class. But I still like the nested class structure. – Ries Jul 25 '12 at 10:12
  • @Ries: Why use the parent `sql` class at all then? Why not just use a module instead? There is no practical reason for you to use the `sql` class there. – Martijn Pieters Jul 25 '12 at 10:14
0

As the title of the question indicated, I really wanted to know how to recursively nest a class definition. I am sure there are many ways to 'skin the fluent-interface-in-python cat'.

I found a way to recursively nest a class definition using decorators:

# class decorator
def recursiveclass(decorated_class):
    decorated_class.__dict__[decorated_class.__name__] = decorated_class
    return decorated_class     

#fluent interface
class sql:
    @recursiveclass
    class select:        
        def __init__(self, dbcolumn, astype=None, asname=None):
            self.dbcolumn = dbcolumn
            self.astype = astype
            self.asname = asname

Please note that I am not saying this is the best way to implement a fluent interface in Python. I am merely experimenting.

To verify:

dir(sql.select.select.select.select)
['__doc__', '__init__', '__module__', 'select']
Ries
  • 2,844
  • 4
  • 32
  • 45
  • 1
    The example above does show how to recursively nest classes, but I am beginning to doubt the usefulness of doing this, as the child class has no access to its parent's attributes. Maybe this makes more sense for defining fluent interfaces: https://gist.github.com/3175416 – Ries Jul 25 '12 at 10:16