In Python, strings are immutable sequences and lists are mutable sequences. Today we will talk more about our mutable sequence, lists, and operations we can use to modify it. We will also discuss the implications of mutability. We will also introduce a new immutable sequence in Python: tuples.
Acknowlegement. This notebook has been adapted from the Wellesley CS111 Spring 2019 course materials (http://cs111.wellesley.edu/spring19).
A Python list is a sequence of values, which are called elements of the list. A list can be created by writing a sequence of comma-separated expressions delimited by square brackets.
primes = [2, 3, 5, 7, 11, 13, 17, 19] # List of primes less than 20
houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'] # A list of four strings
We can access elements of a list using indices. Indices could be expressions or members of other lists.
primes = [2, 3, 5, 7, 11, 13, 17, 19]
primes[6-4] # access the element with the index that evaluates to result of 6-4
# Try to guess first what the output will be
houses[primes[0]]
A list can contain any collection of Python objects, including lists. We call a list of lists a nested list. Here are examples of how we index items in nested lists.
animalLists = [['fox', 'raccoon'], ['duck', 'raven', 'gosling']]
mammals = animalLists[0]
mammals
mammals[1]
animalLists[0][1]
animalLists[1][0]
Unlike integers, strings, floats, which are immutable. Lists are a mutable object in Python and can be changed in place. This has several implications which we will discuss in this lecture.
First, we look at different ways in which we can modify a list in place.
Let us work through these with examples. You can also follow how the state of the list changes with each operation on the lecture slides.
myList = [1, 2, 3, 4] # fresh assignment: creates a new list wiht the name myList
=
¶myList[1] = 7 # changing the value of 1st index of myList by direct assignment
myList
Append. We change lists by appending a new item to the end.
myList.append(5) # appending an item at the end of the list
myList # to see what is in there
Extend. We can append mutiple items to a list at once using the extend method.
myList.extend([6, 8]) # extend method lets you append multiple items (as a list) at once
myList # notice myList now also contains 50 and 71
Pop. We can remove and return the last item from a list using the pop method.
Let's see first what's in the list myList
, which was already mutated in the previous cells in this Notebook:
myList
myList.pop(3) # removes the item at index 3 and returns it
myList
myList.pop() # no index means pop last item and return
myList
Insert. The insert()
method is used to insert an item at a specific index. The items to the right of the index being inserted at shift over to make room.
myList.insert(0, 11) # insert 11 at index 0, shift everything over
myList
len(myList)
myList.insert(10, 12) # if index given is out of bounds, insert at the end
myList[:3]
myList
Remove. The remove()
method is used to remove an item from a specific index. The indices of the remaining items is adjusted accordingly.
myList # current state
myList.remove(12) # .remove(item) removes the item from the list
myList
myList.remove(10) # .remove throws a ValueError if the item is not in list
The sorted function we use for sequences always returns a new list. To sort a list in place, Python has a .sort method which sorts the list by mutating the existing list, not returning a new list.
list1 = [6, 3, 4]
list2 = [6, 3, 4]
list1.sort()
sorted(list2) # sorts and returns a new list
list1 # has changed
list2 # has not changed
Identity vs Value.
is
operator compares the identity of two objects, the id()
function returns an integer representing its identity ==
operator compares the value (contents) of an objectQuestion. Which mutable objects have you encountered so far?
Implications of List Mutability. Because lists are mutable, it is important to understand the implications of mutability.
We can verify whether the identity of two objects is the same or not using the is
operator. var1 is var2
, let's you check whether variables var1
and var2
are aliases, because they point to the same place in memory. Alternatively, Python function id()
, let's you check that two variables are aliases, because they point to the same value. Notice how the two calls below will return the same value, the address in memory of the value.
num = 5 # int object five gets name num
id(num) # memory address num points to
num = num + 1 # expression on right evaluates to 6, gets stored in a new place in memory, num now points to that
id(num) # now is different!
myList = [1, 2, 3]
id(myList)
myList.append(4)
myList
id(myList) # same address as before!
myList = [1, 2, 3] # fresh assignment - > creates a fresh list!
id(myList) # new address
newList = [1, 2, 3] # fresh assignment - > creates a fresh list!
newList is myList # checks if id(newList) == id(myList)
myList == newList #checks if myList has same value as newList
list2 = myList # creates an alias, both point to same address in memory!
list2 == myList #are their contents the same?
list2 is myList # do these two variables point to the same place in memory? yes
list2.append(42) # change list2
myList #myList also changes!
word = "Shikha" # question asked in lecture
newWord = "Shikha"
word is newWord # do they have the same identity?
What is going on? Because strings are immutable data type, Python can and does create temporary aliases but these have no affect because any operation that tries to modify the object will end up breaking the alias link and creating a new object. For example, let us update the variable word
and see if it still has the same identity as newWord
.
word = word + " Singh" # update word
word is newWord # check again, identify is no longer same!
newList1 = sorted(myList)
newList2 = sorted(myList)
newList1 is newList2 # why is this?
newList3 = myList[:]
newList3 is myList
Summary Lists are mutable can be changed in place. Some methods to modify a given list are: append, insert, remove, extend, etc. These methods mutate the list itself. When a list is assigned to another variable, it creates an alias (which points to the same location in memory).
Other general sequence operations (that can be performed on a string as well) such as slicing, the sorted function, etc all return a new sequence and do not modify the original sequence.
What is the value of c[0]
at the end of executing the following statements?
a = [15, 20]
b = [15, 20]
c = [10, a, b]
# c[1] is an alias for (points to) list a
# c[2] is an alias for (points to) list b
b[1] = 5 # updating b, also updates c[1]
c[1][0] = c[0] # updating a[0] (via c[1][0]) to 10
print(a)
print(b)
print(c)
a is not b
Because of aliasing, changing c[1][0]
also changes a[0] and changing b[1] also changes c[2]!
Let's break down some of the code to see what is going on.
a is c[1]
b is c[2]
Although a
and b
seem to have the same value, they occupy different addresses in the memory, so, they are not the same.
a = [15, 20]
b = [15, 20]
a == b # returns true because the content of the variables is the same
a is b # returns false because they are not the same object in memory
a = [15, 20]
c = [10]
c.append(a)
a[1] = 5
print(a)
print(c)
c[1] is a
# We can use operations that work on sequences, like these:
name = 'gryffindor'
print(name[2])
print(name[3:7])
print('do' in name)
name[0] = 's'
name.append('s')
Summary. Strings are NOT mutable, so we cannot perform mutations on them.
A tuple is an immutable sequence of values. It's written using parens rather than brackets.
# A homogeneous tuple of five integers
numTup = (5, 8, 7, 1, 3)
# A homogeneous tuple with 4 strings
houseTup = ('Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin')
# A pair is a tuple with two elements
pair = (7, 3)
# A tuple with one element must use a comma
# to avoid confusion with parenthesized expression
singleton = (7, )
# A tuple with 0 values
emptyTup = ( )
# A tuple without parens, not good practice
noParen = 'a',
On tuples we can use any sequence operations that don't involve mutation:
type(noParen)
len(houseTup)
houseTup[2]
houseTup[1:3]
'Ravenclaw' in houseTup
houseTup*2 + ('Privet Drive',)
Immutable. However, any sequence operation that tries to change a tuple will fail.
houseTup[0] = 'Cupboard Under the Stairs'
houseTup.append('Three Broomsticks')
houseTup.pop(3)
Consider an information tuple with three parts: (1) name (2) age (3) wears glasses?
harryInfo = ('Harry Potter', 11, True)
We can extract name parts of this tuple using three assignments:
name = harryInfo[0]
age = harryInfo[1]
glasses = harryInfo[2]
print('name:', name, 'age:', age, 'glasses:', glasses)
But it's simpler to extract all three parts in one so-called tuple assignment:
(name, age, glasses) = harryInfo
print('name:', name, 'age:', age, 'glasses:', glasses)
Note that the tuple assignment
(name, age, glasses) = harryInfo
is just a concise way to write the three separate assignments:
name = harryInfo[0]
age = harryInfo[1]
glasses = harryInfo[2]
Also note that parens are optional in a tuple assignment, so this works, too:
name, age, glasses = harryInfo
print('name:', name, 'age:', age, 'glasses:', glasses)
print(name.lower(), age + 6, not glasses)
We can also print elements of a list using format printing.
myList
, then *myList
means put the elements of myList
in as argumentsstr
) and catenated with the remaining parts of the format string. "Hello, you {} world{}".format("silly",'!') # creates a new string
print("Hello, {}.".format("you silly world!"))
myList = ['you', 'silly', 'world!']
print(*myList) # note the resulting spaces
print('Hello, {} {} {}'.format(*myList))
print("Hello, {1} {2} {0}".format('you','silly','world!'))
# notice the indices in {}
Summary. Format printing allows us a lot of flexibility in printing and works well with lists as well.