[Python-ideas] Method chaining notation

Ryan Gonzalez rymg19 at gmail.com
Fri Feb 21 22:40:27 CET 2014


On Fri, Feb 21, 2014 at 11:30 AM, Chris Angelico <rosuav at gmail.com> wrote:

> 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)
> ...
>
>
Ugh!


> 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
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>

Seems weird to me, since I'm used to -> being for C++ pointers. I prefer
"..", because it gives the impression that it's something additional.
Either that, or I've used Lua too much.

-- 
Ryan
If anybody ever asks me why I prefer C++ to C, my answer will be simple:
"It's becauseslejfp23(@#Q*(E*EIdc-SEGFAULT. Wait, I don't think that was
nul-terminated."
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20140221/3c79e6ec/attachment.html>


More information about the Python-ideas mailing list