[Python-Dev] PEP 203 Augmented Assignment

Guido van Rossum guido@beopen.com
Thu, 27 Jul 2000 08:37:24 -0500


> I take it you rather see a 'AUGOP' bytecode than a 'GETSET_<type>' bytecode,
> that does the loading, operation and storing ? Not that I disagree, not at
> all, I just want to have that clear ;)

Yes, mostly because there are so many variants based on the kind of
loading and storing: for each load-type operator you need 11 GETSET
operators, and there are many different load operators: local, global,
unspecified (== local by dict instead), by attribute, by index, or by
slice...

I am still neutral on the choice between a single AUGOP with an
argument that takes an argument specifying the opcode, and 11 new
operators: AUGASS_ADD, ...  (Actually, the latter seem the most
logical given that we also have BINARY_ADD, ...)

> Here's what happens now:
> 
> >>> (a, b, c) += 1,2,3
> SyntaxError: no augmented assignment to tuple
> 
> (Isn't a reference implementation a cool thing ? ;)

Very nice.  Don't touch that part.

> > Second, what should happen to a slice assignment?  The basic slice
> > form is:
> 
> >   a[i:j] += b
> 
> > but there are others: Python's slice syntax allows an arbitrary [yadah]
> 
> What's so special about slice assignment ?

The only special thing about slice assignment is that there are 12
extra opcodes to deal with slices in the non-augmented-assignment
case.  I was making a case for avoiding this explosion, but somehow I
got lost in my own train of thought. :(

> You yourself (though Tim<wink>)
> suggested the following:
> 
> x += y
> 
> is effectively (minus order and number of evaluations)
> 
> x = x.__add_ab__(y)
> 
> Where __add_ab__() is the Python method, if 'x' is a python class, or plain
> '__add__' if 'x' doesn't have an '__add_ab__', or 'y.__radd__' if x doesn't
> have __add__. Similarly, if 'x' is a builtin, tp_as_number->nb_inplace_add is
> used, or tp_as_number->nb_add, etc. (Or sq_concat, if it's a seq and the
> operator is add.)
> 
> The __add_ab__ method, or the C equivalent, can decide for itself whether it
> should return 'self', thus making the operation truly in-place, or a new
> instance of an object. This may seem as a complicated and unexpected way to
> do things, but it makes it possible to transparently support __add__ and
> __radd__ too, because they already return new objects.
> 
> And you (through Tim) literally scolded me for trying to suggest
> 
> x[1:10] += y
> 
> being something like
> 
> x = x.__add_slice_ab__(y, slice(1,10))
> 
> Instead, it should become
> 
> x[1:10] = x[1:10] + y
> 
> (Or rather, to keep the same order of evaluation:)
> 
> tmp1 = y # in case of a more complicated expression
> tmp2 = x[1:10]
> x[1:10] = tmp2.__add_ab__(tmp1)
> 
> The whole story about how complicated, convoluted and confusing Python's
> slicing is, though interesting and correct, is not connected to augmented
> assignment ;) 
> 
> >   a[:, ..., ::, 0:10:2, :10:, 1, 2:, ::-1] += 1
> 
> Becomes
> 
> tmp1 = a[:, ..., ::, 0:10:2, :10:, 1, 2:, ::-1]
> a[:, ..., ::, 0:10:2, :10:, 1, 2:, ::-1] = tmp1.__add_ab__(1)

Yes.  I should have realized earlier on that my pseudo code was just
spelling out the necessary DUP etc. opcodes to avoid calculating the
location of 'a' or the index(es) twice.  Looking at my pseudo code for
"a[i:j] += b" again, I realize that there's no reason to treat basic
slices different than in other contexts -- it can be done with just
DUP, ROT3 and ROT4:

  a[i:j] += b

      LOAD a		[a]
      DUP		[a, a]
      LOAD i		[a, a, i]
      DUP		[a, a, i, i]
      ROT3		[a, i, a, i]
      LOAD j		[a, i, a, i, j]
      DUP		[a, i, a, i, j, j]
      ROT4		[a, i, j, a, i, j]
      GETSLICE		[a, i, j, slice]
      LOAD b		[a, i, j, slice, b]
      AUGADD		[a, i, j, result]
      SETSLICE		[]

So we need a new opcode ROT4 (or ROT_FOUR to be consistent in the
spelling).

> > It would be nice if the SLICE bytecodes were removed altogether and
> > instead slice() objects would be created for all slices, even basic
> > ones.  (I believe this was proposed in this list at some point.)  The
> > original SLICE opcodes were introduced in ancient times, when basic
> > slices were the only accepted slice syntax.
> 
> Agreed, however! You can't just make all slices use a sliceobject, for two
> reasons: a C extention type can use both the sequence and the mapping
> interface, and might not expect basic slices to be turned into a slice
> object. For instance, it's mapping interface may raise a TypeError, "Slices
> not supported", when it encounters a slice. Choosing mapping over sequence
> methods may break all that code. The same goes for Python classes.

That's right.  The backwards compatibility issues are the most
daunting.

> The other reason is usability. The current __get_slice__(self, start, end)
> sytnax is very convenient for nearly all uses of slices. Possibly because
> the builtin types don't handle extended slices currently, and possibly
> because the slice-object way is not generally known. (Just recently, there
> was someone asking on python-list on how to use slices. I'm not sure if he
> got a proper answer, but I've never seen such questions on the subject of
> basic slices!)
> 
> If we do get a new __get_slice__ that handles all slices, I suggest doing it
> like this:
> 
> class X:
>     def __get_newslice__(self, (start, end, step)):
> 
> where the type of start, end and step is not defined. This can be done with
> slice objects and sequence-unpacking, of course, but it makes it a lot more
> usable.

Another alternative that tries to preserve compatibility is to call
__getslice__(self, start, end) when the step is None, but
__getslice__(self, start, end, step) when it isn't.  Old code will
raise a reasonable exception when a step is specified, while
step-aware code can specify the step as a default argument value of
1 or None.

--Guido van Rossum (home page: http://www.pythonlabs.com/~guido/)