I thought I understood Python slicing operations, but when I tried to update a sliced list, I got confused:
>>> foo = [1, 2, 3, 4]
>>> foo[:1] = ['one'] # OK, foo updated
>>> foo
['one', 2, 3, 4]
>>> foo[:][1] = 'two' # why foo not updated?
>>> foo
['one', 2, 3, 4]
>>> foo[:][2:] = ['three', 'four'] # Again, foo not updated
>>> foo
['one', 2, 3, 4]
Why isn't foo updated after foo[:][1] = 'two'
?
Update: Maybe I didn't explain my questions clearly. I know when slicing, a new list is created. My doubt is why a slicing assignment updates the list (e.g. foo[:1] = ['one']
), but if there are two levels of slicing, it doesn't update the original list (e.g. foo[:][2:] = ['three', 'four']
).
In short, slicing is a flexible tool to build new lists out of an existing list. Python supports slice notation for any sequential data type like lists, strings, tuples, bytes, bytearrays, and ranges. Also, any new data structure can add its support as well.
“Indexing” means referring to an element of an iterable by its position within the iterable. “Slicing” means getting a subset of elements from an iterable based on their indices.
As well as using slicing to extract part of a list (i.e. a slice on the right hand sign of an equal sign), you can set the value of elements in a list by using a slice on the left hand side of an equal sign. In python terminology, this is because lists are mutable objects, while strings are immutable.
Definition and UsageThe slice() function returns a slice object. A slice object is used to specify how to slice a sequence. You can specify where to start the slicing, and where to end. You can also specify the step, which allows you to e.g. slice only every other item.
foo[:]
is a copy of foo
. You mutated the copy.
This is because python does not have l-values that could be assigned. Instead, some expressions have an assignment form, which is different.
A foo[something]
is a syntactic sugar for:
foo.__getitem__(something)
but a foo[something] = bar
is a syntactic sugar for rather different:
foo.__setitem__(something, bar)
Where a slice is just a special case of something
, so that foo[x:y]
expands to
foo.__getitem__(slice(x, y, None))
and foo[x:y] = bar
expands to
foo.__setitem__(slice(x, y, None), bar)
Now a __getitem__
with slice returns a new list that is a copy of the specified range, so modifying it does not affect the original array. And assigning works by the virtue of __setitem__
being a different method, that can simply do something else.
However the special assignment treatment applies only to the outermost operation. The constituents are normal expressions. So when you write
foo[:][1] = 'two'
it gets expanded to
foo.__getitem__(slice(None, None, None)).__setitem__(1, 'two')
the foo.__getitem__(slice(None, None, None))
part creates a copy and that copy is modified by the __setitem__
. But not the original array.
The main thing to notice here is that foo[:]
will return a copy of itself and then the indexing [1]
will be applied on the copied list that was returned
# indexing is applied on copied list
(foo[:])[1] = 'two'
^
copied list
You can view this if you retain a reference to the copied list. So, the foo[:][1] = 'two'
operation can be re-written as:
foo = [1, 2, 3, 4]
# the following is similar to foo[:][1] = 'two'
copy_foo = foo[:]
copy_foo[1] = 'two'
Now, copy_foo
has been altered:
print(copy_foo)
# [1, 'two', 3, 4]
But, foo
remains the same:
print(foo)
# [1, 2, 3, 4]
In your case, you didn't name the intermediate result from copying the foo
list with foo[:]
, that is, you didn't keep a reference to it. After the assignment to 'two'
is perfomed with foo[:][1] = 'two'
, the intermediate copied list ceases to exist.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With