What range function does to a Python list? - python

I am not able to figure out what is happening here. Appending reference to range function is kind of creating a recursive list at index 3.
>>> x = range(3)
[0, 1, 2]
>>> x.append(x)
[0, 1, 2, [...]]
>>> x[3][3][3][3][0] = 5
[5, 1, 2, [...]]
Whereas, when I try this:
>>> x = range(3)
[0, 1, 2]
>>> x.append(range(3))
[0, 1, 2, [0, 1, 2]]
I can easily deduce the reason for the second case but not able to understand what appending reference to range function is doing to the list appended.

In python2, ranges are lists.
lists, and most of the things in python are objects with identities.
li = [0,1]
li[1] = li # [0, [...]]
# ^----v
id(li) # 2146307756
id(li[1]) # 2146307756
Since you're putting the list inside itself, you're creating a recursive data structure.

First this is strange, and you probably should not use this in practice. The issue is not specific to range function and has to do with references. When you call x.append(x), you essentially say that x[-1] is x. So when you modify x[0], you also modify x[-1][0], x[-1][-1][0] etc.
To see that this is not range specific, you could use copy.copy:
from copy import copy
x = range(1)
x.append(x) # here x[1] is reference to x itself (same object)
print(x[0], x[1][0], x[1][1][0])
x[0] = 1
print(x[0], x[1][0], x[1][1][0]) # all values change
#
x = range(1)
x.append(copy(x)) # x[1] is a copy of x at previous state (new object)
print(x[0], x[1][0]) # cannot call x[1][1][0] -> x[1][1] is an int
x[0] = 1
print(x[0], x[1][0]) # only the first value changes
Output:
(0, 0, 0)
(1, 1, 1)
(0, 0)
(1, 0)

Related

Chain assignment in Python for list

I am trying to understand chain assignment in Python.
If I run x = x[1] = [1, 2], I get an infinite list [1, [...]].
But if I run x = x[1:] = [1, 2], I will get a normal list [1, 1, 2].
How does it work in the background to make these two different results?
First, understand that in a chained assignment, the right-most expression is evaluated to an object. A reference to that object is then assigned to each target in sequence, from left to right. x = y = z is effectively the same as
t = z # A new temporary variable "t" to hold the result of evaluating z
x = t
y = t
del t
Second, you need to understand the difference between assigning t to the subscription x[1] and to the slicing x[1:]. In the former, element 1 of x is replaced by t. In the latter, elements x[1], x[2], etc are replaced by t[0], t[1], etc, extending or shrinking x as necessary.
The first example is equivalent to
t = [1,2]
x = t
x[1] = t
del t
x[1] becomes a reference to x itself; list.__repr__ can detect cycles like this and represents them with .... x, x[1], x[1][1], etc are all references to the original list [1,2].
The second example is equivalent to
t = [1,2]
x = t
x[1:] = t
del t
In this case, no cycles are created. x[1] is replaced by a reference to 1, not x/t itself, and x is extended to make x[2] a reference to 2. (If it helps, think of x[1:] = t as producing the same result as x = [x[0]]; x.extend(t).)
x = x[1] = [1, 2]
In this case, x and x[1] are the same object. Therefore x contains itself, which is represented by ... in your output.
x = x[1:] = [1, 2]
In this case, x is a slice, and slices are copies. so they are not the same object.
Explaining the outputs:
x = x[1] = [1, 2] # Output: [1, [...]]
The reason is that the list is not copied, but referenced.
x[1] is a reference to the list [1, 2].
When you assign x[1] to x, you are assigning a reference to the list [1, 2] to x.
So x and x[1] are both references to the same list.
When you change the list through one of the references, the change appears in the other reference as well. This is called aliasing.
x = x[1:] = [1, 2] # Output: [1, 1, 2]
The reason is that the list is copied, not referenced.
x[1:] is slice of the list [1, 2].
The slice is a copy of the list, not a reference to the list.
When you assign x[1:] to x, you are assigning a copy of the list [1, 2] to x.
So x and x[1:] are both copies of the same list.
When you change the list through one of the references, the change does not appear in the other reference.

Query regarding call by value and call by reference in Python

I am quite confused, which case is correct, first case or second case or both the cases are correct in Python?
Why in first case the list x remain unchanged?
why in second case the list x also got modified?
Case 1:
x = [0, 1, 2]
def cleanIt(y):
y = []
return y
print(cleanIt(x)) #[]
print(x) #[0, 1, 2]
Case 2:
x = [0, 1, 2]
def appendOne(z):
z.append(4)
return z
print(appendOne(x)) #[0, 1, 2, 4]
print(x) #[0, 1, 2, 4]
Python behaves like pass-by-object-reference. Therefore what you really pass into the functions are actually references to the objects you give. In the first example, you pass a reference that points to the memory address in which x is present. However when you do the assignment operation y = [], you simply change the memory address your reference points to. You do not change the content of x. In the second example, you call the method append(). In that case, you call this method for the object that is present in the memory address pointed to by your reference.
These may come confusing at the beginning, but you should remember that assigning your variable directly to something else doesn't actually change your variable, it changes where your reference points to. However using a method, modifying your variable updates it.
I have used id() to give you a clear idea of the references. id() returns the memory address of the argument passed to it.
Case-1
x = [0, 1, 2]
print(id(x))
def cleanIt(y):
print(id(y))
y = []
print(id(y))
return y
print(cleanIt(x))
print(x)
Output:
139677941202304
139677941202304
139677940574528
[]
[0, 1, 2]
You pass the reference of x to cleanIt.
y = [] - Creates a new list altogether. This has nothing to do with x. See the references above.
return y - You return the newly created list i.e, y = []
Since x is unchanged, it will retain its initial values.
Case-2
x = [0, 1, 2]
print(id(x))
def appendOne(z):
print(id(z))
z.append(4)
print(id(z))
return z
print(appendOne(x))
print(x)
Output
139677940574912
139677940574912
139677940574912
[0, 1, 2, 4]
[0, 1, 2, 4]
You pass the reference of x to appendOne(). x and z point to the same list.
z.append(4) - Appends a 4 to list referenced by z.
Since z and x refer to the same list, changes made in z reflects in x too. See the references above.
In Python values are sent to functions by means of object reference.
Please read this to know more about this concept.
Edit:
If I want to have x unchanged while z should be [0,1,2,4], then how should I modify the python code?
This is how you can do.
x = [0, 1, 2]
print(id(x))
def appendOne(z):
print(id(z))
# Creates a new list with contents of x.
z = list(z)
z.append(4)
print(id(z))
return z
print(appendOne(x))
print(x)
Output
140368975642112
140368975642112
140368975011392
[0, 1, 2, 4]
[0, 1, 2]

Iterate over all partial "head" lists of a list

Is there a "more Pythonic" way than:
l=list(range(5)) # ANY list, this is just an example
list(l[:i] for i in range(1, len(l)))
Out[14]: [[0], [0, 1], [0, 1, 2], [0, 1, 2, 3]]
E.g. not using the index.
In C++ one can construct a sequence using a pair of (start, end) iterators. Is there an equivalent in Python?
To be clear, if you're aiming for "Pythonic", I think your current example of using indices are what most Python programmers would do.
That being said, you also mentioned creating an object using a pair of (start, end) values. Python has slice objects, which is what the square bracket indices (the object's __getitem__ call) internally uses. It can be created using the builtin slice function:
>>> my_list = [0, 1, 2, 3, 4]
>>> slice1 = slice(0, 3)
>>> slice1.start
0
>>> slice1.stop
3
>>> my_list[slice1]
[0, 1, 2]
>>> slice2 = slice(1, 2)
>>> my_list[slice2]
[1]
Of course this works only if my_list can be indexed. If you want this to work for iterables in general, #Hackaholic's answer using itertools.islice is what I would use.
And yes, this still means you will need to use the square bracket index eventually. The difference here is you're storing the (start, stop) value of the partial heads in objects you can use to actually create the partial heads.
Now to come back to your example:
>>> slices = [slice(0, x) for x in range(len(any_list))]
>>> partial_heads = [any_list[slc] for slc in slices]
you can use itertools.islice:
>>> import itertools
>>> l=list(range(5))
>>> [list(itertools.islice(l, x)) for x in range(1,len(l))]
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3]]
you can check time of execution: There two factor memory performance and speed.
>>> timeit.timeit('[list(itertools.islice(l, x)) for x in range(1,len(l))]', setup='l=list(range(5))')
3.2744126430006872
>>> timeit.timeit('list(l[:i] for i in range(1, len(l)))', setup='l=list(range(5))')
1.9414149740005087
Here you go:
>>> [list(range(x)) for x in range(1, 5)]
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3]]

Python in place object update in a for loop

In Python >= 3.5:
x = np.zeros((2,3))
for x_e in x:
x_e += 123
This operation returns a 2x3 matrix of all 123's. Whereas the following returns all zeros:
x = np.zeros((2,3))
for x_e in x:
x_e = 123
This is a little off putting to me since x_e is an element from x and it doesn't totally feel like x is being updated.
Okay, I think this is a thing, and it is called 'in place' update? (similar to an in place algorithm?)
But, the jarring thing is this does not work with lists:
x = [0,0,0]
for x_e in x:
x_e += 123
This returns the list
[0, 0, 0]
I would appreciate if someone can enlighten me on what precisely is happening here.
In the first snippet you perform an in-place addition on a ndarray object. Since each x_e is an ndarray the inplace operation succeeds and alters the elements.
In the second snippet you're just reassinging to the loop-variable. Nothing is performed on the elements of x.
In the third snippet you don't have a multidimentional list so each x_e is actually an int. Using += on an int doesn't change in-place, it returns a new integer object (which you don't store).
The following might be more related to the first:
x = [[0, 0, 0] for _ in range(3)]
for x_e in x:
x_e += [123]
here each element of x is a list object [0, 0, 0] on which you add, in-place, the element 123. After executing, x will be:
[[0, 0, 0, 123], [0, 0, 0, 123], [0, 0, 0, 123]]
Imagine you have that:
>>> x = np.array(range(5))
>>> x
array([0, 1, 2, 3, 4])
So now:
>>> x+123
array([123, 124, 125, 126, 127])
As you can see the '+' action is mapped to the array. So when you do
create an array full of 0's and add 123 to the sub lists the array is consisted of is logical to have the above result.

python appending a list to a tuple

I have following tuple:
vertices = ([0,0],[0,0],[0,0]);
And on each loop I want to append the following list:
[x, y]
How should I approach it?
You can't append a list to a tuple because tuples are "immutable" (they can't be changed). It is however easy to append a tuple to a list:
vertices = [(0, 0), (0, 0), (0, 0)]
for x in range(10):
vertices.append((x, y))
You can add tuples together to create a new, longer tuple, but that strongly goes against the purpose of tuples, and will slow down as the number of elements gets larger. Using a list in this case is preferred.
You can't modify a tuple. You'll either need to replace the tuple with a new one containing the additional vertex, or change it to a list. A list is simply a modifiable tuple.
vertices = [[0,0],[0,0],[0,0]]
for ...:
vertices.append([x, y])
You can concatenate two tuples:
>>> vertices = ([0,0],[0,0],[0,0])
>>> lst = [10, 20]
>>> vertices = vertices + tuple([lst])
>>> vertices
([0, 0], [0, 0], [0, 0], [10, 20])
You probably want a list, as mentioned above. But if you really need a tuple, you can create a new tuple by concatenating tuples:
vertices = ([0,0],[0,0],[0,0])
for x in (1, 2):
for y in (3, 4):
vertices += ([x,y],)
Alternatively, and for more efficiency, use a list while you're building the tuple and convert it at the end:
vertices = ([0,0],[0,0],[0,0])
#...
vlist = list(vertices)
for x in (1, 2):
for y in (3, 4):
vlist.append([x, y])
vertices = tuple(vlist)
At the end of either one, vertices is:
([0, 0], [0, 0], [0, 0], [1, 3], [1, 4], [2, 3], [2, 4])
Not sure I understand you, but if you want to append x,y to each vertex you can do something like :
vertices = ([0,0],[0,0],[0,0])
for v in vertices:
v[0] += x
v[1] += y

Categories