On Fri, Feb 21, 2014 at 11:30 AM, Chris Angelico <rosuav@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@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."