[Python-ideas] Method chaining notation

Chris Angelico rosuav at gmail.com
Fri Feb 21 18:30:03 CET 2014


Yeah, I'm insane, opening another theory while I'm busily championing
a PEP. But it was while writing up the other PEP that I came up with a
possible syntax for this.

In Python, as in most languages, method chaining requires the method
to return its own object.

class Count:
    def __init__(self): self.n = 0
    def inc(self):
        self.n += 1
        return self

dracula = Count()
dracula.inc().inc().inc()
print(dracula.n)

It's common in languages like C++ to return *this by reference if
there's nothing else useful to return. It's convenient, it doesn't
cost anything much, and it allows method chaining. The Python
convention, on the other hand, is to return self only if there's a
very good reason to, and to return None any time there's mutation that
could plausibly return a new object of the same type (compare
list.sort() vs sorted()). Method chaining is therefore far less common
than it could be, with the result that, often, intermediate objects
need to be separately named and assigned to. I pulled up one file from
Lib/tkinter (happened to pick filedialog) and saw what's fairly
typical of Python GUI code:

...
        self.midframe = Frame(self.top)
        self.midframe.pack(expand=YES, fill=BOTH)

        self.filesbar = Scrollbar(self.midframe)
        self.filesbar.pack(side=RIGHT, fill=Y)
        self.files = Listbox(self.midframe, exportselection=0,
                             yscrollcommand=(self.filesbar, 'set'))
        self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
...

Every frame has to be saved away somewhere (incidentally, I don't see
why self.midframe rather than just midframe - it's not used outside of
__init__). With Tkinter, that's probably necessary (since the parent
is part of the construction of the children), but in GTK, widget
parenting is done in a more method-chaining-friendly fashion. Compare
these examples of PyGTK and Pike GTK:

# Cut down version of http://pygtk.org/pygtk2tutorial/examples/helloworld2.py
import pygtk
pygtk.require('2.0')
import gtk

def callback(widget, data):
    print "Hello again - %s was pressed" % data

def delete_event(widget, event, data=None):
    gtk.main_quit()
    return False

window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_title("Hello Buttons!")
window.connect("delete_event", delete_event)
window.set_border_width(10)
box1 = gtk.HBox(False, 0)
window.add(box1)
button1 = gtk.Button("Button 1")
button1.connect("clicked", callback, "button 1")
box1.pack_start(button1, True, True, 0)
button2 = gtk.Button("Button 2")
button2.connect("clicked", callback, "button 2")
box1.pack_start(button2, True, True, 0)
window.show_all()

gtk.main()

//Pike equivalent of the above:
void callback(object widget, string data) {write("Hello again - %s was
pressed\n", data);}
void delete_event() {exit(0);}

int main()
{
    GTK2.setup_gtk();
    object button1, button2;
    GTK2.Window(GTK2.WINDOW_TOPLEVEL)
        ->set_title("Hello Buttons!")
        ->set_border_width(10)
        ->add(GTK2.Hbox(0,0)
            ->pack_start(button1 = GTK2.Button("Button 1"), 1, 1, 0)
            ->pack_start(button2 = GTK2.Button("Button 2"), 1, 1, 0)
        )
        ->show_all()
        ->signal_connect("delete_event", delete_event);
    button1->signal_connect("clicked", callback, "button 1");
    button2->signal_connect("clicked", callback, "button 2");
    return -1;
}


Note that in the Pike version, I capture the button objects, but not
the Hbox. There's no name ever given to that box. I have to capture
the buttons, because signal_connect doesn't return the object (it
returns a signal ID). The more complicated the window layout, the more
noticeable this is: The structure of code using chained methods
mirrors the structure of the window with its widgets containing
widgets; but the structure of the Python equivalent is strictly
linear.

So here's the proposal. Introduce a new operator to Python, just like
the dot operator but behaving differently when it returns a bound
method. We can possibly use ->, or maybe create a new operator that
currently makes no sense, like .. or .> or something. Its semantics
would be:

1) Look up the attribute following it on the object, exactly as per
the current . operator
2) If the result is not a function, return it, exactly as per current.
3) If it is a function, though, return a wrapper which, when called,
calls the inner function and then returns self.

This can be done with an external wrapper, so it might be possible to
do this with MacroPy. It absolutely must be a compact notation,
though.

This probably wouldn't interact at all with __getattr__ (because the
attribute has to already exist for this to work), and definitely not
with __setattr__ or __delattr__ (mutations aren't affected). How it
interacts with __getattribute__ I'm not sure; whether it adds the
wrapper around any returned functions or applies only to something
that's looked up "the normal way" can be decided by ease of
implementation.

Supposing this were done, using the -> token that currently is used
for annotations as part of 'def'. Here's how the PyGTK code would
look:

import pygtk
pygtk.require('2.0')
import gtk

def callback(widget, data):
    print "Hello again - %s was pressed" % data

def delete_event(widget, event, data=None):
    gtk.main_quit()
    return False

window = (gtk.Window(gtk.WINDOW_TOPLEVEL)
    ->set_title("Hello Buttons!")
    ->connect("delete_event", delete_event)
    ->set_border_width(10)
    ->add(gtk.HBox(False, 0)
        ->pack_start(
            gtk.Button("Button 1")->connect("clicked", callback, "button 1"),
            True, True, 0)
        ->pack_start(
            gtk.Button("Button 1")->connect("clicked", callback, "button 1"),
            True, True, 0)
    )
    ->show_all()
)

gtk.main()


Again, the structure of the code would match the structure of the
window. Unlike the Pike version, this one can even connect signals as
part of the method chaining.

Effectively, x->y would be equivalent to chain(x.y):

def chain(func):
    def chainable(self, *args, **kwargs):
        func(self, *args, **kwargs)
        return self
    return chainable

Could be useful in a variety of contexts.

Thoughts?

ChrisA


More information about the Python-ideas mailing list