[Python-ideas] Dart-like method cascading operator in Python

Steven D'Aprano steve at pearwood.info
Fri Nov 22 03:16:39 CET 2013


On Thu, Nov 21, 2013 at 11:55:46AM +0100, Perešíni Peter wrote:
> Recently I fell in love with Dart method cascading operator .. to the
> degree that I find it really convenient and I am missing it in the Python.
> The cascade operator
> 
> myObj
>   ..setX(5)
>   ..y=6
> 
> is the syntactic sugar equivalent of the following:
> 
> tmp = myObj
> tmp.setX(5)
> tmp.y=6
> 
> This can be used to greatly simplify instantiating objects and can also
> enable creating domain specific languages in Python.

Please give examples of how this might do such a thing, rather than just 
claim it will. "This syntax will end world hunger and bring peace to the 
Middle East..." I don't see how this syntax can simplify instantiating 
objects:

obj = MyClass("spam", "eggs")

is already pretty simple. Perhaps you're referring only to a small 
subset of (in my opinion) *poorly designed* if not outright buggy 
objects which aren't instantiated completely on creation and need to be 
tweaked by hand before being ready to use. Or those with excessively 
complicated APIs that could really do with a few helper functions.

In my opinion, we shouldn't encourage classes that require manual 
instantiation like this:

obj = MyClass()
obj.foo = "spam"
obj.setbar("eggs")
obj.make_it_work()
# now obj is fully instantiated and ready to be used...

not even with your suggested syntax:

obj = MyClass()
  ..foo = "spam"
  ..setbar("eggs")
  ..make_it_work()


Helping people do the wrong thing is not, in my opinion, an advantage.

The use-case of DSLs is perhaps more interesting, but a simple example 
would go a long way to support the idea.


> In particular, we can
> make this much more powerful in Python (as opposed to Dart) because Python
> recognizes scope by indentation and therefore it would be possible to do
> something like
> 
> gnuplot.newPlot()
>  ..set("xrange [0:5]")
>  ..set("yrange [0:20]")
>  ..newPlot()
>      ..addSeries("Linear", [1,2,3])
>      ..addSeries("Quadratic", [1,4,6])
>  ..run()

I had to read that multiple times before I was able to interpret what 
this is supposed to mean. It doesn't help that I'm not familiar enough 
with the gnuplot API to tell exactly what you're doing. I *think* that 
it would be the equivalent of this:

p = gnuplot.newPlot()
p.set("xrange [0:5]")
p.set("yrange [0:20]")
q = p.newPlot()
q.addSeries("Linear", [1,2,3])
q.addSeries("Quadratic", [1,4,6])
p.run()

If I'm wrong, I think that suggests that your extension to the syntax 
isn't as clear as you hoped. If I'm right, I wonder what the point of 
the inner sub-block is, since you don't appear to do anything with the q 
plot. What have I missed?



[...]
> Note that this is strictly better than method chaining, e.g.
> 
> template = MyObject().setX(1).setY(2)
> 
> because
> a) method chaining is hard to write on multiple lines, the only two options
> are
[snip explicit line continuation]
> (template = MyObject()
>     .setX(1)
>     .setY(2))
> 
> which looks weird.

I suggest it only looks weird because you've put the opening bracket in 
the wrong place. I'd write it like this:

template = (MyObject()
    .setX(1)
    .setY(2)
    )

a a variation of such, which apart from an extra set of parentheses is 
virtually exactly the same as your suggestion:

template = MyObject()
    ..setX(1)
    ..setY(2)


without the addition of a new double-dot syntax.

To the extent that this suggested cascading dot operator is just used 
for method chaining, I'm against it because we can already easily chain 
methods with no new syntax provided the object supports chaining.

To the extent that it adds chaining to those objects which don't 
support it, for example those with mutator methods that return None, 
that's a more interesting proposal:

mylist = list(some_function(a, b, c))
    ..sort()
    ..reverse()
    ..pop()  # output of pop is discarded
    ..sort()  # still operating on the list


But I'm going to suggest that this is still not a good idea, since it 
goes against the Zen "Explicit is better than implicit". As I understand 
it, this cascade relies on there being an implicit "last object used", 
and that can be a bit problematic.

For starters, you should explain precisely how the compiler will 
determine what is the "last object used" in practice. Your examples 
so far suggest it is based on the last line of source code, but 
that's tricky. For example:

x = spam(); y = eggs()
  ..method()

I would *assume* that this implicitly calls y.method rather than 
x.method, but that's not a given. How about this?

y = spam()
x = eggs()
del y
  ..method()

Does that call x.method()? And what do this do?

myClass()
for i in range(3):
    ..method()

Is it a syntax error? Or does it call i.method()? Or perhaps even call 
method on the range object? Or does the myClass object still count as 
the implicit "last object used"?

You're going to need to specify exactly where and under what 
circumstances the implicit object is set, and where you can use this 
implicit object cascading operator.


> b) method chaining cannot take advantage of multiple scoping, e.g. the only
> way to write Gnuplot example is

Since I'm not sure I understand your Gnuplot example above, I'm not 
going to further comment on it here.


> c) method chaining needs specialized API, e.g. each method needs to return
> self

This is a good argument in support of your proposal.


> d) method chaining cannot be used with attributes, e.g. there is no
> equivalent to
> 
> obj = MyObject()
>   ..x=1
>   ..y=2

That's a good argument in support of your proposal, but not a terribly 
powerful argument, since the above is trivially written as:

obj = MyObject()
obj.x = 1
obj.y = 2


and I for one prefer to see the object being operated on explicitly 
rather than implicitly.



-- 
Steven


More information about the Python-ideas mailing list