Container types

  • There are several default containers in Python which allow a single variable to contain multiple variables. Many specialised ones exist in the Python standard library but the four built-in ones are:
    • Lists
    • Tuples
    • Dictionaries
    • Sets

Lists

  • These are ordered containers for variables. They are similar to arrays in C but more closely resemble vectors in C++. They can contain any number or type of Python objects and a list does not have to contain only one type of Python object.
  • As mentioned previously, some Python objects are mutable, meaning that their value can be altered without copying to a new Python object. Lists are one of these mutable objects.
  • You can add, remove and change the objects in a list at will and it will change size to accomodate the elements dynamically.
  • You can access individual elements of a list by using its index. In Python indices always start at zero like C.

In [1]:
l = []  # Can create an empty list
l = ["a","b","c","d","e","f"]  # Defining a simple list of strings
print(l)  # Like all Python objects you can print them directly for information
print(l[0])  # Accessing an index directly uses '[i]' notation
print(l[4])


['a', 'b', 'c', 'd', 'e', 'f']
a
e

In [2]:
l.append("g")  # The append member function (or 'method') let's you add an element
               # to the end of the list.
print(l)


['a', 'b', 'c', 'd', 'e', 'f', 'g']
  • Firstly, note that the append() method is called from the list variable, it is not an outside function taking a list as an argument like the print function.
  • Now the important difference in behaviour between the append() and extend() methods.

In [3]:
l = ["a","b","c","d","e","f"]
l.append(["g","h","i","j"])  # Appending a list to a list
print(l)
print()
l = ["a","b","c","d","e","f"]
l.extend(["g","h","i","j"])  # Extending a list
print(l)


['a', 'b', 'c', 'd', 'e', 'f', ['g', 'h', 'i', 'j']]

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
  • Appending will always put whatever Python object at the end of the list, even if it's another list.
  • The extend method only takes a list as an argument. It then loops through the elements of the list and appends each element in turn.
  • You can also insert/remove an element at an index and remove specific valued elements.

In [4]:
l = ["a","c"]
print(l)
l.insert(1,"b")  # Puts object at an index and shunts all equal and higher elements upwards
print(l)

# You can query what the index is for a particular value
print(l.index("c"))

# Now what happens when you remove a value at an index
popped = l.pop(2)  # the 'pop(2)' actually returns the value of the removed object
print(l)
print(popped)
# Could have used 'pop()' with no argument to remove the last element by default.

# Can also remove by value
l = ["a", "b", "c", "b"]
l.remove("b")  # Removes the first instance of the 'b' value, leaves all others
print(l)


['a', 'c']
['a', 'b', 'c']
2
['a', 'b']
c
['a', 'c', 'b']

In [5]:
l = [["a", "b"], ["c", "d", "e"]]  # Nesting lists is easy and doesn't have to be square
print(l[0][1])  # Accessing by index is evaluated left->right
print(l[1][2])


b
e
  • Indices for lists don't have to be positive.

In [6]:
l = ["a", "b", "c", "d", "e"]
print(l[-1])


e
  • Arithmetic operators have meaning for lists.

In [7]:
l = ["a", "b"]
print(l*3)  # Creates a new list of repeating elements
print(l + ["c", "d"])  # Creates a new list with elements from second list appended


['a', 'b', 'a', 'b', 'a', 'b']
['a', 'b', 'c', 'd']
  • Subsets of lists can be creating using the slicing syntax.

In [8]:
l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(l[0:3])  # Slicing uses indices and notation [from : to : step(optional)]
print(l[4:9:2])
print(l[4:])  # Leaving out either side of the colon implies all elements
print(l[5:-2])  # Negative indices still viable
print(l is not l[:])  # Can use slicing to return a copy of the list rather than the object


[0, 1, 2]
[4, 6, 8]
[4, 5, 6, 7, 8, 9]
[5, 6, 7]
True

Tuples

  • Tuples are very similar to lists, in that they are ordered containers and you can access their values by index.
  • However they are immutable meaning that they cannot be updated once created. Essentially a read-only list.
  • Tuples use parentheses () rather than square brackets [] to initialise themselves.

In [9]:
t = ()  # Can create an empty tuple
t = ("a", "b", "c", "d", "e", "f")
print(t)
print(t[1]) # Access index
print(t[0:2])  # Tuples can also slice to return a new tuple
# t[1] = "k"  # Not allowed as tuples are immutable


('a', 'b', 'c', 'd', 'e', 'f')
b
('a', 'b')
  • Note though that by using a mutable type (list) as an element in a tuple, we can get a bit tricky and change the value of an element in a tuple, as below.

In [10]:
t = ([1,1],[2,2])
print(t)
t[0][0]=2
print(t)


([1, 1], [2, 2])
([2, 1], [2, 2])
  • Tuples actually have another way to be initialised, using a Python concept Tuple Packing.
  • Tuple packing is creating a simple (1D) tuple from multiple values. Tuple unpacking takes a tuple and sets each of its elements equal to some new variables.
  • Putting comma separated values on the LHS means unpacking, comma separated values on the right means packing up into a tuple.

In [11]:
t = 1,2,3  # Pack integers into a tuple
print(t)
one, two, three = t  # Unpack tuple values into new variables.
print(one,two,three)


(1, 2, 3)
1 2 3

In [12]:
t = [1,2,3]
# Can also unpack lists/any iterable object, but be careful of unordered dicts/sets!
# Don't need to specify variables for all elements of tuple/list.
# Use the '*' to indicate multiple values placed into one variable
one, *rest = t  
print(one,rest)


1 [2, 3]
  • This syntax also lets you update multiple variables at once, rather than having to assign temporary variables. In Python this is faster than using multiple lines of assignment.

In [13]:
a, b = 1, 2
a, b = b, a+b  # Update, the RHS is fully evaluated before the LHS
print(a,b)


2 3

Dictionaries

  • These are another mutable type but the elements are unordered.
  • They are essentially a hash table with key-value pairs, very similar to a C++ map.
  • They are initialised with curly-braces {} and comma separated key : value pairs.
  • They allow you to return a value (any Python object) by accessing using a key (any immutable Python object), which is often a number or string.

In [14]:
ages = {}  # An empty dictionary
ages = {"David":27, "Andy":28, "Emma":4}  # Initialise with keys and values
print(ages["David"])  # Return the value by using the key

ages["Darcey"] = 2  # Can add/change keys
print(ages["Darcey"])


27
2
  • Can get a 'view object' (called dict_keys) of the keys or values at will, which are dynamically updated as the dictionary changes.

In [15]:
viewKeys = ages.keys()  # Similar object with values() method
print(viewKeys)
ages["Rosie"] = 58
print(viewKeys)  # View object is updated without having to called keys() again


dict_keys(['Andy', 'Emma', 'Darcey', 'David'])
dict_keys(['Andy', 'Emma', 'Rosie', 'Darcey', 'David'])
  • Seems a useless object now, just wait.

Sets

  • This container is again an unordered collection of elements and is mutable.
  • Cannot have duplicate elements of a set.
  • Initialised with curly-braces {}.
  • Good for comparisons between different sets of variables as in mathematics.

In [16]:
s1 = {"a", "b", "c", "d"}  # Initialised
print(s1)
s1.add("e")  # Can add and remove elements
s1.remove("e")
s2 = {"c", "d", "e", "f"}
# Several mathematical operations
print(s1.union(s2))
print(s1.difference(s2))


{'c', 'd', 'b', 'a'}
{'e', 'c', 'b', 'd', 'a', 'f'}
{'b', 'a'}
  • Note that the elements won't necessarily be in any particular order.

Casting to iterables

  • Containers are also iterable objects, meaning that their elements can be looped over sequentially, which we'll cover later.
  • Iterable objects have many special properties, one of which is the casting between them.

In [17]:
l = [0,1,2,3,4]
t = tuple(l)  # Cast iterable to tuple
s = set(l)   # Cast iterable to set
d = dict([["a", 1], ["b", 2]])  # Cast iterable of pairs to dictionary
print(d)
name = "Dave"
l = list(name)  # Strings are iterable
print(l)


{'b': 2, 'a': 1}
['D', 'a', 'v', 'e']

Deletion of variables

  • It's also possible to delete variable names to remove them and free their memory.
  • In general you don't have to worry about memory (de)allocation in Python as it has an in-built Garbage-collector which frees up memory when objects no-longer have a variable name assigned to them e.g. When variables go out of scope.
  • However you may want to manually clean up your local variable names or delete elements in lists/dictionaries directly.
  • To do this use the del keyword.

In [18]:
a = 1
b = [0,1,2,3,4]
c = {"Dave":27, "Andy":28}
del a
#print(a)  # This won't work as 'a' no longer exists

del b[2]  # Delete by index
print(b)
del b  # Delete whole object

del c["Dave"]  # Delete by key
print(c)
del c  # Delete whole object


[0, 1, 3, 4]
{'Andy': 28}