I tried this code at the REPL, in 3.8:
>>> a = list(range(10))
>>> a[:] = (i for i in a for _ in range(2))
We are assigning to elements of a
based on elements from a generator, and that generator is iterating over a
, and we don't even have a one-to-one correspondence of elements. That seems an awful lot like modifying the list while iterating over it, so I expected that this would go poorly in one way or another.
But instead, it works exactly according to naive expectation:
>>> a
[0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9]
After a moment's thought, it seems that Python has to make some kind of temporary copy before actually doing the assignment. After all, the inserted slice could be a different size from the replaced slice (as long as it isn't an extended slice), which would require shifting elements from after the slice; and there's no way to know how far to shift them without evaluating the generator.
However, it's easy to imagine an implementation of that which would still encounter a problem. For example: copy elements after the slice to a temporary; mark from the beginning of the slice onwards as unused; append elements from the generator per the usual .append
logic; finally .extend
with the temporary. (Of course, that wouldn't work for extended slices, but extended slices can't resize the list anyway.) With that implementation, our example would hit an IndexError
immediately, because the list would be cleared before the generator even starts being used.
So: is the actual behaviour reliable/guaranteed? Is it version-specific? How exactly does Python implement the slice assignment?