Reputation: 1023
I was trying to modify the values in lists via slices and for-loops, and ran into some pretty interesting behavior. I would appreciate if someone could explain what's happening internally here.
>>> x = [1,2,3,4,5]
>>> x[:2] = [6,7] #slices can be modified
>>> x
[6, 7, 3, 4, 5]
>>> x[:2][0] = 8 #indices of slices cannot be modified
>>> x
[6, 7, 3, 4, 5]
>>> x[:2][:1] = [8] #slices of slices cannot be modified
>>> x
[6, 7, 3, 4, 5]
>>> for z in x: #this version of a for-loop cannot modify lists
... z += 1
...
>>> x
[6, 7, 3, 4, 5]
>>> for i in range(len(x)): #this version of a for-loop can modify lists
... x[i] += 1
...
>>> x
[7, 8, 4, 5, 6]
>>> y = x[:2] #if I assign a slice to a var, it can be modified...
>>> y[0] = 1
>>> y
[1, 8]
>>> x #...but it has no impact on the original list
[7, 8, 4, 5, 6]
Upvotes: 1
Views: 788
Reputation: 13858
Let's break down your comments 1 by 1:
1.) x[:2] = [6, 7]
slices can be modified:
See these answers here. It's calling the __setitem__
method from the list
object and assigning the slice
to it. Each time you reference x[:2]
a new slice object is created (you can simple do id(x[:2])
and it's apparent, not once will it be the same id).
2.) indices of slices cannot be modified:
That's not true. It couldn't be modified because you're performing the assignment on the slice
instance, not the list
, so it doesn't trigger the __setitem__
to be performed on the list
. Also, int
are immutable so it cannot be changed either way.
3.) slices of slices cannot be modified:
See above. Same reason - you are assigning to an instance of the slice and not modifying the list
directly.
4.) this version of a for-loop cannot modify lists:
z
being referenced here is the actual objects in the elements of x
. If you ran the for loop with id(z) you'll note that they're identical to id(6), id(7), id(3), id(4), id(5)
. Even though list
contains all 5 identical references, when you do z = ...
you are only assigning the new value to the object z
, not the object that is stored in list
. If you want to modify the list
, you'll need to assign it by index, for the same reason you can't expect 1 = 6
will turn x
into [6, 2, 3, 4, 5]
.
5.) this version of a for-loop can modify lists:
See my answer above. Now you are directly performing item assignment on the list
instead of its representation.
6.) if I assign a slice to a var, it can be modified:
If you've been following so far, you'll realize now you are assigning the instance of x[:2]
to the object y
, which is now a list
. The story follows - you perform an item assignment by index on y
, of course it will be updated.
7.) ...but it has no impact on the original list:
Of course. x
and y
are two different objects. id(x) != id(y)
, therefore any operation performed on x
will not affect y
whatsoever. if you however assigned y = x
and then made a change to y
, then yes, x
will be affected as well.
To expand a bit on for z in x:
, say you have a class foo()
and assigned two instances of such to the list f
:
f1 = foo()
f2 = foo()
f = [f1, f2]
f
# [<__main__.foo at 0x15f4b898>, <__main__.foo at 0x15f4d3c8>]
Note that the reference in question is the actual foo
instance, not the object f1
and f2
. So even if I did the following:
f1 = 'hello'
f
# [<__main__.foo at 0x15f4b898>, <__main__.foo at 0x15f4d3c8>]
f
still remains unchanged since the foo instances remains the same even though object f1
now is assigned to a different value. For the same reason, whenever you make changes to z
in for z in x:
, you are only affecting the object z
, but nothing in the list is changed until you update x
by index.
If however the object have attribute or is mutable, you can directly update the referenced object in the loop:
x = ['foo']
y = ['foo']
lst = [x,y]
lst
# [['foo'], ['foo']]
for z in lst:
z.append('bar')
lst
# [['foo', 'bar'], ['foo', 'bar']]
x.append('something')
lst
# [['foo', 'bar', 'something'], ['foo', 'bar']]
That is because you are directly updating the object in reference instead of assigning to object z
. If you however assigned x
or y
to a new object, lst
will not be affected.
Upvotes: 4
Reputation: 662
There is nothing odd happening here. Any slice that you obtain from a list is a new object containing copies of your original list. The same is true for tuples.
When you iterate through your list, you get the object which the iteration yields. Since int
s are immutable in Python you can't change the state of int
objects. Each time you add two int
s a new int
object is created. So your "version of a for-loop [which] cannot modify lists" is not really trying to modify anything because it will not assign the result of the addition back to the list.
Maybe you can guess now why your second approach is different. It uses a special slicing syntax which is not really creating a slice of your list and allows you to assign to the list (documentation). The newly created object created by the addition operation is stored in the list through this method.
For understanding your last (and your first) examples, it is important to know that slicing creates (at least for lists and tuples, technically you could override this in your own classes) a partial copy of your list. Any change to this new object will, as you already found out, not change anything in your original list.
Upvotes: 0