[Python-ideas] Method chaining notation

Masklinn masklinn at masklinn.net
Mon Feb 24 20:59:26 CET 2014


On 2014-02-24, at 17:08 , Ron Adam <ron3200 at gmail.com> wrote:
> On 02/24/2014 09:12 AM, Paul Moore wrote:
>> On 24 February 2014 15:01, Ron Adam<ron3200 at gmail.com>  wrote:
>>> >You would probably see it used more often like this...
>>> >
>>> >    def names(defaults, pos_names, pos_args, kwds):
>>> >        return  {}.=update(defaults) \
>>> >                  .=update(zip(pos_names, pos_args) \
>>> >                  .=update(kwds)
>> How is this better than
>> 
>>     def names(defaults, pos_names, pos_args, kwds):
>>         ret = {}
>>         ret.update(defaults)
>>         ret.update(zip(pos_names, pos_args)
>>         ret.update(kwds)
>>         return ret
>> 
>> (I originally named the return value _ to cater for the tendency to
>> insist on punctuation rather than names in this thread, but honestly,
>> why*not*  name the thing "ret"?)
>> 
>> I get the idea of chained updates, I really do. But translating
>> between mutation of a named value and chained updates is pretty
>> trivial, so I don't see how this is anything but a case of "follow the
>> preferred style for the language/API you're using". And Python uses
>> updating named values, why is that so bad?
> 
> It's not bad, just not as good.  The chained expression is more efficient and can be used in places where you can't use more than a single expression.
> 
> The point is to maintain both a visual and computational separation of mutable and immutable expressions.
> 
> Compare the byte code from these. You can see how the chained version would be more efficient.

The chained version is not intrinsically more efficient, the Python
compiler could be smart enough to not LOAD_FAST (ret) repeatedly (if
that proves beneficial to execution speed, which I'm not even certain
of, and either way it's going to be extremely minor compared to the
actual cost of executing methods). AFAIK the peephole optimiser does
not even bother eliding out pairs of STORE_FAST $name LOAD_FAST $name
e.g. as far as I know

    a = foo()
    a.bar()

compiles to:

              0 LOAD_*                   0 (foo) 
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
              6 STORE_FAST               0 (a) 

              9 LOAD_FAST                0 (a) 
             12 LOAD_ATTR                1 (bar) 
             15 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
             18 POP_TOP

the pair (6, 9) is a noop and could trivially be removed (in the absence
of jumps around). According to [0] a patch implementing this (although
without taking care of jumps) was rejected:

> because apparently the additional six lines of code didn’t buy
> enough of a speed improvement for an uncommon case.

(although no link to the patch so he might have been optimizing the
triplet of (STORE_FAST, LOAD_FAST, RETURN_VALUE)). If removing 2
bytecode instructions once in a while does not sway the core team, I
really can't see removing a single one even more rarely doing so.

> By using the '.' we can see the difference.  The byte code should be very close to this, even though this function will give an error if you try to run it.  (Can't update None.)  The actual difference would probably be replacing LOAD_ATTR with LOAD_MUTATE_ATTR, Which would call __getmutatemethod__ instead of __getmethod__.  (or something similar to that, depending on how it's implemented.)

Why? There's no need for LOAD_MUTATE_ATTR. And LOAD_ATTR calls
__getattribute__ (and __getattr__ if necessary), a bound method is a
form callable attribute, the bytecode for a method call (assuming an
object on the stack) is

    LOAD_ATTR $attrname
    CALL_FUNCTION

that the function mutates the original object (or not) has no relevance
to attribute loading.

> def names(defaults, pos_names, pos_args, kwds):
>    return {}.update(defaults) \
>             .update(zip(pos_names, pos_args)) \
>             .update(kwds)
> 
> >>> dis(names)
>  2           0 BUILD_MAP                0
>              3 LOAD_ATTR                0 (update)
>              6 LOAD_FAST                0 (defaults)
>              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
>             12 LOAD_ATTR                0 (update)
> 
>  3          15 LOAD_GLOBAL              1 (zip)
>             18 LOAD_FAST                1 (pos_names)
>             21 LOAD_FAST                2 (pos_args)
>             24 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
>             27 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
>             30 LOAD_ATTR                0 (update)
> 
>  4          33 LOAD_FAST                3 (kwds)
>             36 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
>             39 RETURN_VALUE

That bytecode's not correct for the case:

* the return value of each method call needs to be discarded with a POP_TOP
* LOAD_ATTR needs an object on the stack so you need a DUP_TOP before each
  LOAD_ATTR (update) (you can create the correct bytecode with something
  like byteplay, it'll work)

[0] http://www.coactivate.org/projects/topp-engineering/blog/2008/11/03/optimizing-python/


More information about the Python-ideas mailing list