CodingBison

Simply put, a Python list is a sequence of elements. Each of these elements can be of different types. Lists are mutable and thus, we can change the values of list elements in-place. This provides great flexibility in terms of creating a list, adding elements to it, and deleting elements from it.

A list is defined by keeping a set of elements, each separated by a comma and then enclosing these elements within square brackets ("[" and "]"). As an example, we can define a list with elements as "varList = [237, 80, 280, 101]".

We can reference elements of a list using their index. The indexing is 0-based and so, the first element has an index 0, the second element has an index 1, and so on. We can also specify a negative index, where the value specifies the position of the element from the end. Thus an index of -1 means the first element from the last, an index of -2 means the second element from the last, and so on.

The following figure shows a Python list (varList) that has five elements, each of different type. Figure: A Python list

Since list contains many elements, we can use the len() function to get the number of elements present in a list. We provide below an example that defines a list and accesses its elements.

[user@codingtree]\$ python3
Python 3.2.1 (default, Jul 11 2011, 18:55:33)
[GCC 4.6.1 20110627 (Red Hat 4.6.1-1)] on linux2
>>>
>>> varList = [237, 80, 280, 101]
>>>
>>> print(varList)
[237, 80, 280, 101]
>>> print(type(varList))
<class 'list'>
>>>
>>> print(len(varList))
4
>>> print(varList)
80
>>> print(varList[-1])
101
>>>

Let us use Python's range() method to create a new list. range(n) returns a sequence of integers from 0 to (n-1). range(n,m) returns a list of integers starting from n but less than m. Also, n should be less than m, else the output of range() would be empty.

The range() method returns a sequence that is not of the list type -- to convert it into a list, we need to use the list() function. Here is an example:

>>> varRange = range(10)
>>> print(type(varRange))
<class 'range'>
>>> print(varRange)
range(0, 10)
>>>
>>> varList = list(range(10))
>>> print(type(varList))
<class 'list'>
>>> print(varList)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
>>> varList = -10
>>> print(varList)
[-10, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

Lists can be modified using Python slicing. For a list (let us say, varList), varList[i:j] means all the elements starting at (and including) index i but before (and excluding) the index j. Slicing also supports negative indexing. varList[i:-k] means starting at (and including) index i but before (and excluding) the kth element the end. If the index i,j are missing, then it is assumed that they are the start and end of the list. Thus, varList[0:k] is same as varList[:k] and varList[0:-k] is same as varList[:-k]

>>> varList = list(range(10))
>>>
>>> print(varList)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
>>> print(varList[0:5])
[0, 1, 2, 3, 4]
>>>
>>> print(varList[1:5])
[1, 2, 3, 4]
>>>
>>> print(varList[1:-5])
[1, 2, 3, 4]
>>>
>>> print(varList[:-5])
[0, 1, 2, 3, 4]
>>>
>>> print(varList[5:2])
[]
>>>
>>> print(varList[:])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

Python also allows large negative or positive number as indices. Instead of returning an index error, it automatically trims the indices.

>>> varList = list(range(5))
>>>
>>> print(varList[:1000])
[0, 1, 2, 3, 4]
>>> print(varList[-10000:])
[0, 1, 2, 3, 4]
>>> print(varList[10000:10000])
[]
>>> print(varList[-10000:-10000])
[]
>>> print(varList[-10000:10000])
[0, 1, 2, 3, 4]
>>> print(varList[10000:-10000])
[]
>>>

Next, we can use the "*" operator to form a new list by repeating a given list. List repetition can be very handy in trying to create lists and assigning values later. Let us consider the following example:

>>> varList = list(range(5))
>>>
>>> print(varList)
[0, 1, 2, 3, 4]
>>>
>>> varListNew = varList * 3
>>> print(varListNew)
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
>>>
>>> varListNone = [None] * 5
>>> print(varListNone)
[None, None, None, None, None]
>>>
>>> varListNone = "tiger"
>>> varListNone = "guanaco"
>>> varListNone = "zebra"
>>> varListNone = "mountain lion"
>>> print(varListNone)
['tiger', 'guanaco', 'zebra', 'mountain lion', None]
>>>

Lists provide the ability to loop over each element using the "for" construct. Also, elements belonging to a list can also be quickly tested using the "in" construct:

>>> varList = ["tiger", "guanaco", "zebra", "mountain lion"]
>>>
>>> for elements in varList:
...     print(elements)
...
tiger
guanaco
zebra
mountain lion
>>> if "zebra" in varList:
...     print("zebra is present in varList")
...
zebra is present in varList
>>>

However, the list implementation may not be very efficient when doing a lookup with large number of elements. This is because the above search essentially scans the list linearly. An implementation using dictionaries (which is actually a hash-table) provides a faster lookup as compared to a list lookup.

List Comprehensions

Yet another way to create lists is to use list comprehensions. List comprehensions typically have three parts, all enclosed within square brackets: "[property_of_elem for elem in S]", where for each element, elem, of the sequence, S, we provide some property of element, property_of_elem. The list comprehension can also contain an optional if clause towards the end, which then acts as a filter for elements present in S.

Here is a simple example that builds a list of first 10 square numbers. In the example that is provided below, the "x in range(10)" clause implies all numbers belonging to range(10). Since range() function excludes the last number, this means that x belongs to the set of numbers starting from 0 till (and including) 9. The "x * x" clause means that for each of these numbers, we take their square and then add these (square) values to the new list objList.

objList = [x * x for x in range(10)]

print(type(objList))
print(objList)

The output (provided below) shows that objList is also type of list.

<class 'list'>
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

We can use the "if" case, if we wanted to be more specific and filter out some of the elements. Here is an example of a list comprehension that creates odd numbers from number 1 to N. The output of this code would be "[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]".

objList = [x for x in range(20) if x % 2 != 0]
print(objList)

Lastly, if needed, we can also have multiple instances of both for and if. This allows us to pack more logic in these comprehensions. Let us see this in action using an example. The example (provided below) creates tuples from two sets of numbers using both list comprehension and a regular for-loop; the list-comprehension uses two for clauses.

objList1 = [(x,y) for x in range(1,4) for y in range(1001, 1004)]
print(objList1)

objList2 = []
for x in range(1,4):
for y in range(1001,1004):
objList2.append((x,y))
print(objList2)

For both approaches, the output shows the same combinations of numbers from (1, 2, 3) and (1001, 1002, 1003).

[(1, 1001), (1, 1002), (1, 1003), (2, 1001), (2, 1002), (2, 1003), (3, 1001), (3, 1002), (3, 1003)]
[(1, 1001), (1, 1002), (1, 1003), (2, 1001), (2, 1002), (2, 1003), (3, 1001), (3, 1002), (3, 1003)]

Slicing, shallow copying, and deep copying

Slicing serves a special purpose for lists. When we assign a list to a new list, then Python simply assigns a reference of the earlier list object to the new list. Thus, when we modify the one of the two lists, then we also end up modifying the other! Sometimes, we would like to avoid that and make a true copy of the existing list rather than by sharing a reference to the common list object. For such cases, we can use slice (and slice the entire list) and assigned the sliced copy to a new list object.

The example (provided below) shows that modifying the new list (varListReference) also ends up modifying the original list (varList). However, with slice(), we do make a new copy (varListCopy) and modifying the new copy leaves the original list intact.

>>> varList = list(range(5))
>>> varListReference = varList
>>> varListReference = 237
>>> print(varList)
[237, 1, 2, 3, 4]
>>>
>>> varList = list(range(5))
>>> varListCopy = varList[:]
>>> varListCopy = -237
>>> print(varList)
[0, 1, 2, 3, 4]
>>>

However, a copy using slice() method is a shallow copy -- this means that it does not recursively make a copy of all the inner elements. Instead, it simply keeps a reference of those elements from the first list in the new list.

>>> varSmallList = list(range(5))
>>> varList = [varSmallList, 101, 280]
>>> print(varList)
[[0, 1, 2, 3, 4], 101, 280]
>>>
>>> varListCopy = varList[:]
>>> print(varListCopy)
[[0, 1, 2, 3, 4], 101, 280]
>>>
>>> #Now, modify the varSmallList and both lists would be modified.
... varSmallList = 680
>>>
>>> print(varList)
[[680, 1, 2, 3, 4], 101, 280]
>>> print(varListCopy)
[[680, 1, 2, 3, 4], 101, 280]
>>>
>>>

To avoid this, we can use the deepcopy method provided by the copy module. We also make a true copy of the inner elements (varSmallList) in this case. As a result, modifying varSmallList leads to modification of varList only and the varListDeepcopy remains unmodified.

>>> varSmallList = list(range(5))
>>> varList = [varSmallList, 101, 280]
>>> print(varList)
[[0, 1, 2, 3, 4], 101, 280]
>>>
>>> import copy
>>> varListDeepcopy = copy.deepcopy(varList)
>>> print(varListDeepcopy)
[[0, 1, 2, 3, 4], 101, 280]
>>>
>>> #Now, modify the varSmallList and only the first list would be modified.
... varSmallList = 680
>>>
>>> print(varList)
[[680, 1, 2, 3, 4], 101, 280]
>>> print(varListDeepcopy)
[[0, 1, 2, 3, 4], 101, 280]
>>>

Note that the copy module also provides a copy() method for shallow-copying. For lists, this is equivalent to copying using the slicing.