12

I came across the following code (sort of):

my_list = [1, [2, 3, 4], 5]
my_list[1:2] = my_list[1]

After running these two lines, the variable my_list will be [1, 2, 3, 4, 5]. Pretty useful for expanding nested lists.

But why does it actually do what it does?

I would have assumed that the statement my_list[1:2] = my_list[1] would do one of the following:

  • simply put [2, 3, 4] into the second position in the list (where it already is)
  • give some kind of "too many values to unpack" error, from trying to put three values (namely 2,3,4) into a container of only length 1 (namely my_list[1:2]). (Repeating the above with a Numpy array instead of a list results in a similar error.)

Other questions (e.g. How assignment works with python list slice) tend to not pay much attention to the discrepancy between the size of the slice to be replaced, and the size of the items you're replacing it with. (Let alone explaining why it works the way it does.)

acdr
  • 4,538
  • 2
  • 19
  • 45
  • `my_list = [1,2,3]; my_list[1:2] = [9,8,7]` => `[1, 9, 8, 7,3]` I thought it's usual behaviour of python while replacing slice with list – splash58 Dec 29 '17 at 13:09
  • Use this `my_list[1:2] = [my_list[1]]` – Artier Dec 29 '17 at 14:00
  • I edited one of the answers there to explicitly mention that you can use different-sized lists on both sides of the assignment. *"Let alone explaining why it works the way it does."* What kind of answer do you expect? https://stackoverflow.com/a/10623352/2301450 and https://stackoverflow.com/a/35632876/2301450 are both correct. – vaultah Dec 29 '17 at 15:45
  • I was hoping for an answer detailing a mechanic of the language that I misunderstood, or wasn't familiar with (an underlying reason for why this should work, like "containers are implemented as double-linked lists, so setting a single element to multiple elements automatically works because blah blah"). The real answer turns out to be a very mundane "because we made this work magically for lists, but nothing else". (I was afraid this would be the answer when I noticed that it doesn't work for Numpy arrays.) – acdr Dec 29 '17 at 16:10

6 Answers6

5

Slice assignment replaces the specified part of the list with the iterable on the right-hand side, which may have a different length than the slice. Taking the question at face value, the reason why this is so is because it's convenient.

You are not really assigning to the slice, i.e. Python doesn't produce a slice object that contains the specified values from the list and then changes these values. One reason that wouldn't work is that slicing returns a new list, so this operation wouldn't change the original list.

Also see this question, which emphasizes that slicing and slice assignment are totally different.

nnnmmm
  • 7,964
  • 4
  • 22
  • 41
  • This answer is similar to some of the others, except for one sentence, which I think is very important: "the reason why this is so is because it's convenient". List slice assignment was made specifically to have this behaviour, not necessarily because it's logical (I think it isn't logical at all) but because it's convenient. Thanks! – acdr Dec 29 '17 at 13:50
2

Short Answer:

my_list[1:2] = my_list[1] will replaced the content from 1st index to 2nd index of my_list with the content of present in 1st index of my_list

Explanation:

Let's see two slicing operations, very similar but totally distinct

  1. This creates the copy of list and stores it the variable

    some_variable = some_list[2:5]
    
  2. This replaces the content of the list inplace, which permits changing the length of the list too.

    some_list[2:5] = [1, 2, 3, 4]
    

When you use assignment operator =, it invokes a __setitem__ function. Our focus here is the case 2 above. As per the Python's Assignment Statement document:

If the target is a slicing: The primary expression in the reference is evaluated. It should yield a mutable sequence object (such as a list). The assigned object should be a sequence object of the same type. Next, the lower and upper bound expressions are evaluated, insofar they are present; defaults are zero and the sequence’s length. The bounds should evaluate to integers. If either bound is negative, the sequence’s length is added to it. The resulting bounds are clipped to lie between zero and the sequence’s length, inclusive. Finally, the sequence object is asked to replace the slice with the items of the assigned sequence. The length of the slice may be different from the length of the assigned sequence, thus changing the length of the target sequence, if the target sequence allows it.

In our case my_list[1:2] = my_list[1], python will also call __setitem__ as:

my_list.__setitem__(slice(1,2,None), [2, 3, 4])

Refer slice document to know what it does.

So, when you did my_list[1:2] = my_list[1], you replaced the content from 1st index to 2nd index of my_list with the content of present in 1st index of my_list i.e. [2, 3, 4].


I think now we can answer why your assumptions are incorrect:

  • put [2, 3, 4] into the second position in the list (where it already is)

No. Because __setitem__ is not called on the index but on the slice of the indices which you passed.

  • give some kind of "too many values to unpack" error, from trying to put three values (namely 2,3,4) into a container of only length 1 (namely my_list[1:2]).

Again No. Because the range of your indices creating your container is replaced with new set of values.

Moinuddin Quadri
  • 46,825
  • 13
  • 96
  • 126
2

Here is the relevant bit from the Python Language Reference

If the target is a slicing: The primary expression in the reference is evaluated. It should yield a mutable sequence object (such as a list). The assigned object should be a sequence object of the same type. Next, the lower and upper bound expressions are evaluated, insofar they are present; defaults are zero and the sequence’s length. The bounds should evaluate to integers. If either bound is negative, the sequence’s length is added to it. The resulting bounds are clipped to lie between zero and the sequence’s length, inclusive. Finally, the sequence object is asked to replace the slice with the items of the assigned sequence. The length of the slice may be different from the length of the assigned sequence, thus changing the length of the target sequence, if the target sequence allows it.

This behavior makes sense qualitatively because when you slice a list you get a sub list so replacing that with another list shouldn't add a level of nesting. Allowing it to change the length of the list is a design choice. Other choices are possible as your numpy example demonstrates.

Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
1

What you are doing is slice assignment.

Assignment to slices is also possible, and this can even change the size of the list or clear it entirely

my_list[1:2] = my_list[1]

This replaces the slice of my_list with the contents of my_list[1].

By specifying my_list[1:2] on the left side of the assignment operator =, you are telling Python you want to use slice assignment.

my_list[1:2] = my_list[1] is equivalent to my_list.__setitem__(slice(1, 2, None), my_list[1])

In slice(1, 2, None), 1 is start, 2 is stop, and None is step and is optional.

srikavineehari
  • 2,502
  • 1
  • 11
  • 21
1

What you are trying here is called Slice Assingnment. In python it is possible to assign an iterable(my_list[1] in your case) to a slice of another iterable(my_list[0:1] in your case). Lets walk through some examples to understand what it really means:

 >>> l = [1,2,3,4,5]
 >>> b = [6,7,8]
 >>> l[0:3] = b
 >>> l
 >>> [6, 7, 8, 4, 5]

So what happened here is the portion of list l for 0,1,2 indices is which covers elements 1,2,3is replaced by elements of list b 6,7,8. However in this case size of slice and replaced elements happens to be equal by chance.

So what happens when slice size and iterable to be replaced are not equal

>>> l = [1,2,3,4,5]
>>> b = [6,7,8]
>>> l[0:4] = b
>>> l 
>>> [6,7,8,5]

Notice that this operation didn't produce any error, instead, it just copied whatever elements are available with the entire sliced portion. In this case, sliced elements are 1,2,3,4 replaced by 6,7,8

In the previous example iterable to be replaced was smaller. What happens if slice portion is smaller

>>> l = [1,2,3,4,5]
>>> b = [6,7,8]
>>> l[0:1] = b
>>> l 
>>> [6,7,8,2,3,4,5]

So now we can see that only first element is replaced by entire iterable b.

You can also use this behaviour to remove a specific portion of the list ( Which I find convenient in some situations ).

 >>> l = [1,2,3,4,5]
 >>> l[0:2] = []
 >>> l 
 >>> [3,4,5]

First two elements are removed very conveniently here.

So the example in your question is similar to the examples I posted above, except that in your case there is an additional step of unpacking list values. Unpacking list value happens every time when you assign list to another list. A short example

>>> l = [[1]]
>>> a = []
>>> a = l[0]
>>> a
>>> [1]

Your example now:

#Replace the slice [0:1] with my_list[1] perform unpacking list values as well
>>> my_list[1:2] = my_list[1]  
>>> [1,2,3,4,5]

Also note that slice assignment is only possible if you assign iterable to a slice. If you try to assign an int or something that is not an iterable to a slice python will throw an error.

>>> l = [1,2,3,4,5]
>>> b = [6,7,8]
>>> l[0:1] = b[1]
>>> Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
 TypeError: can only assign an iterable

That's why in your case my_list[1] don't raise an error since it is an iterable.

Sohaib Farooqi
  • 5,457
  • 4
  • 31
  • 43
0

That what you are doing is insert an element through slicing. I will explain everything by parts. More than an inserting it could be interpreted as adding an item to your list after slicing target list in a range desired. Now to explain every line in detail:

my_list[1:2]

That part is like saying to Python; "I want to get the values (slice) from index 1 to the index before 2 (excluding 2, I will explain with another example later on)". After that you assign a value to those values:

my_list[1:2] = my_list[1] #The same as my_list[1:2] = [2,3,4]

Now that you know what the first part does, next it is going to add the item at the right side of the '=' operator so you could interpret it like this; "I want to slice from index 1 to everything before 2 (again, excluding index 2) and then add my list [2,3,4]". Now here comes another examples so you understand even better I hope.

problemList = [1, [2, 3, 4], 5]
problemList[1:2] = problemList[1] #The same as problemList[1:2] = [2,3,4]
analogProblemL = [1] + [2,3,4] + [5] #Output : [1,2,3,4,5]

insertList = [12,13,14]
myList = [1, [2, 3, 4], 5,6,7,8,9]
myList[3:6] = insertList
analogFstList = [1,[2,3,4] ,5] + insertList + [9] #Output : [1,[2,3,4],5,12,13,14,9]

myScnList = [1, [2, 3, 4], 5]
myScnList[1:3] = [2,3,4]
analogScnList = [1] + [2,3,4] + [5] #Output : [1,2,3,4,5]

The next lines will be like if it was an animation frames so it's easier to interpret:

[1,2,3,4,5] #List before line of code: myList[1:3] = [12,13,14]
[1,|2,3|,4,5] #The moment where you slice the list with: myList[1:3]. Please notice that the characters used '|' are for representing the slice.
[1] + [12,13,14] + [4,5] #After assigning what the slice should be changed for. It's like excracting from the whole list the values [2,3] and changing it for [12,13,14].
[1,12,13,14,4,5] #Final list after running the code.

Some references used for this answer: http://effbot.org/zone/python-list.htm Understanding Python's slice notation How assignment works with python list slice

Hope it was useful for you.

ccolin
  • 304
  • 2
  • 7