[Python-ideas] Method chaining notation

Yury Selivanov yselivanov.ml at gmail.com
Fri Feb 21 21:08:39 CET 2014


I suggest you to take a look at cascades in Dart language
And this article: http://en.wikipedia.org/wiki/Method_cascading

Yury


On 2/21/2014, 12:30 PM, Chris Angelico 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)
> ...
>
> 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/



More information about the Python-ideas mailing list