3

I define this function to do: [1,2,3] --> [2,3,1]

def shift_to_left(p):
    p.append(p[0])
    return p[1:]       

When I check like this, results are ok:

p1 = [1,2,3]
print p1
p1 = shift_to_left(p1)
print p1

The result:
[1, 2, 3]
[2, 3, 1]

However, when I introduce another list and concatenate as I go the result is different:

ss = []
p1 = [1,2,3]
ss.append(p1)
p1 = shift_to_left(p1)
ss.append(p1)

print ss

The result
[[1, 2, 3, 1], [2, 3, 1]]

But I want: 
[1,2,3]
[2,3,1]

why is it happening?

Thanks very much,

Amadan
  • 191,408
  • 23
  • 240
  • 301
Elyor
  • 33
  • 5
  • 2
    `return p[1:]` doesn't modify the original list. – Barmar Jan 06 '15 at 01:36
  • 1
    Others have explained what is going on in a number of ways, so I will just mention the simplest correct implementation of your function: `return p[1:] + [p[0]]` – zch Jan 06 '15 at 01:42
  • Examine the ids: print `id(p1)` before and after your calls, and the ids of the contents of `ss`. – Michael Urman Jan 06 '15 at 02:42
  • Thanks very much guys, it is my first time to post questions. I am really impressed with answers and dynamic community. – Elyor Jan 06 '15 at 13:36

5 Answers5

4

If you want to shift/rotate elements in a list, I think better would be to use a deque, rather than reinvent the wheel. For example:

from collections import deque
d = deque([1,2,3])
d.rotate(-1)
print(d) 
#[2, 3, 1]
Marcin
  • 215,873
  • 14
  • 235
  • 294
  • Solves the problem but doesn't answer the question ("why") :) Meh, +1 anyway. – Amadan Jan 06 '15 at 01:41
  • 1
    Although I agree, the question concerns itself with "why". Questions like these are very commonly used in university exams. Hence, having a solid understanding of why as opposed to alternatives is very important, especially in this case especially. – Dair Jan 06 '15 at 01:43
  • 1
    @Amadan YOu right, I think other anwsers, answer the OP question pretty well. But using deque is probably faster, easier, less prone to errors, and generally more recommended way for what OP is trying to achieve. – Marcin Jan 06 '15 at 01:43
4

In Python, most arguments are taken by reference.

Your function, shift_to_left, actually mutates its argument (through the use of append), but then returns a slice (which is a shallow copy of the list).

When you replace your original variable with the output of shift_to_left, this behaviour is hidden:

In [1]: def shift_to_left(p):
   ...:     p.append(p[0])
   ...:     return p[1:]
   ...: 

In [2]: xs = [1, 2, 3]

In [3]: xs = shift_to_left(xs)

In [4]: xs
Out[4]: [2, 3, 1]

But if we instead assign the result into a new variable, we can see that the original list has indeed been changed:

In [5]: ys = shift_to_left(xs)

In [6]: ys
Out[6]: [3, 1, 2]

In [7]: xs
Out[7]: [2, 3, 1, 2]

Our result, ys, is the slice of xs from the second element onwards. That's what you expected.

But xs itself has also been changed by the call to append: it's now one element longer than before. This is what you're experiencing in your second example.


If you do not want this behaviour, one way of avoiding it is by passing a copy of your list to shift_to_left:

In [8]: zs = shift_to_left(ys[:])

In [9]: zs
Out[9]: [1, 2, 3]

In [10]: ys
Out[10]: [3, 1, 2]

Here, you can see that the original list ys has not been modified, as shift_to_left was given a copy of it, not the object itself. (This is still passing by reference, of course; it's just not a reference to ys).


Alternatively, and probably more reasonably, you could change shift_to_left itself, so that it does not modify its arguments:

def shift_to_left(xs):
    return xs[1:] + xs[0]  # build a new list in the return statement

The big problem with both these approaches is that they create lots of copies of lists, which can be incredibly slow (and use a lot of memory) when the lists are large.


Of course, as @Marcin points out, if this is more than an academic exercise, you should probably use one of the built-in data structures such as deque.

sapi
  • 9,944
  • 8
  • 41
  • 71
  • "In Python, most arguments are taken by reference" is not correct. In Python, all arguments are passed by assignment (or by object, if you will). See http://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference for explanations. – Kirk Strauser Jan 06 '15 at 02:29
  • @KirkStrauser Unless I'm misunderstanding that post, it's saying that passing by assignment *is* by reference for mutable types (ie, for most of them). – sapi Jan 06 '15 at 02:46
  • The biggest thing to remember is that you're passing in the object that the variable points to, not the variable itself. It's a subtle but important difference. – Kirk Strauser Jan 06 '15 at 03:50
3

If you run your code here, you can notice that ss remains pointing to the original (mutated in your shift function because of p.append(p[0])) copy of p1, where as p1 points to a knew list all together when it gets reassigned, resulting in the behavior. (Step 10 out of 11)

p is mutated

(p becomes mutated, and ss[0] = p)

p1 gets assigned to a new list altogether

(p1 gets assigned to a new list altogether, which is latter appended to ss)

Dair
  • 15,910
  • 9
  • 62
  • 107
  • 2
    That's a pretty sweet educational tool. – Amadan Jan 06 '15 at 01:52
  • @Amadan: This thing has saved my life sooo many times. Especially with pointer related stuff like this. I am so grateful for the person who made this. – Dair Jan 06 '15 at 02:09
1

why is it happening?

return p[1:] is "non-destructive": it creates a new list. However, p.append(p[0]) is "destructive": it changes p itself.

First you append p1 to ss. This makes [[1, 2, 3]], where [1, 2, 3] is p1.

Then you do your shift_to_left, which changes p1 to [1, 2, 3, 1] and returns [2, 3, 1]. Because p1 is contained in ss, your ss becomes [[1, 2, 3, 1]], and then you append the new p1 to form [[1, 2, 3, 1], [2, 3, 1]].

A better implementation would be purely non-destructive:

def shift_to_left(p):
    return p[1:] + [p[0]]
Amadan
  • 191,408
  • 23
  • 240
  • 301
0

Try this:

  p1 = [1,2,3]
  p1 = shift_to_left(p1)
  ss = []
  ss.extend(p1)

  print ss

That prints [2, 3, 1]. Use extend() instead because append() will create an array in an array. Also you had an extra call to ss.append(p1).

FourScore
  • 171
  • 8
  • He *wants* an array in an array, the desired solution (as badly-formatted-but-still-stated) is `[[1, 2, 3], [2, 3, 1]]`. – Amadan Jan 06 '15 at 01:46