List Basics
Python works great with both string (str) and numeric data (int and float). However, games often have a lot of data, and we need a way of organizing it. For example, if you’re writing an adventure game, you might need to keep track of the main character’s inventory – which is simply a list of the useful (or not-so-useful) objects they’ve picked up in their travels. Python provides an incredibly powerful tool for storing lots of information called a list.
Like a string, a list is a sequence object. While strings only contain a sequence of characters, a list may contain a sequence of any valid Python data type, including other lists! Each item stored in a list is called an element.
You’ll recall in U2-4 we used the list() and range() functions to create a list of integers. You can also create a list by typing it out directly:
1 2 |
odd_numbers = list(range(1, 10, 2)) even_numbers = [2, 4, 6, 8, 10] |
The first statement uses the range() function to generate an iterable object and the list() function to convert that into the list [1, 3, 5, 7, 9]. The variable odd_numbers then refers to that list in memory. The second statement creates a list of even numbers more manually and then assigns the variable even_numbers to it.
You can also use the repetition operator (*) to easily create a list with a specific number of elements, each with the same value. Here is an example:
1 |
zero_list = [0] * 100 # creates [0, 0, 0, 0, 0, ..., 0] with 100 elements. |
As with the characters in a string, one can use a for loop to iterate over the elements in a list:
1 2 3 4 |
heroes = [ "Java Man", "Python Girl", "Vik-20" ] for hero in heroes: print("Never fear,", hero, "is here!") |
Similarly, indexing and slicing a list works the same as with strings, including the use of negative indexes. A slice of a list is called a sublist. An IndexError exception will be raised if you use an invalid index when indexing a list. When slicing, invalid indexes are ignored the same way as with string slicing.
1 2 3 4 5 6 7 |
my_list = [10, 20, 30, 40, 50] print(my_list[0], my_list[4], my_list[-3]) # displays: 10 50 30 print(my_list[1:4]) # displays: [20, 30, 40] print(my_list[3:9]) # displays: [40, 50] print(my_list[5:3]) # displays: [] print(my_list[5]) # IndexError Exception! |
You can use the in (or not in) operator to check whether an element is a member of a sequence (or not). Since strings and lists are both sequences, you can use the in operator with both.
1 2 3 4 5 6 7 8 |
heroes = [ "Java Man", "Python Girl", "Vik-20" ] person = input("What hero are you looking for: ") if person in heroes: print("Never fear,", person, "is here!") else: print("Oh no, you're doomed!") |
Lists are Mutable
So far, lists seem to work exactly like strings, except they can store more than simple characters. But there is one important difference! Unlike strings, lists are mutable which means that individual elements can be changed. For example, this works with lists:
1 2 3 4 5 |
my_list = [10, 20, 30, 40, 50] my_list[2] = 99 print(my_list) # displays [10, 20, 99, 40, 50] |
This raises an interesting challenge. Let’s say we want to write a for loop that will double all of the elements in my_list. We could try this:
1 2 3 4 5 6 |
my_list = [10, 20, 30, 40, 50] for item in my_list: item *= 2 print(my_list) # STILL displays [10, 20, 99, 40, 50] |
But this will not change any of the elements in my_list since the item variable is assigned a copy of each element. What we need is to iterate over a range of index values, and then use these to modify the elements in my_list, like so:
1 2 3 4 5 6 |
my_list = [10, 20, 30, 40, 50] for index in range(len(my_list)): my_list[index] *= 2 print(my_list) # displays [20, 40, 60, 80, 100] |
Here I’ve used the range() function to generate a sequence of valid index values from 0 up to (but not including) the length of the list. In other words, the index variable will iterate over the sequence [0,1,2,3,4] in this case. Also, because I’ve used the len() function, my_listcan be any number of elements and my code will still work. Beautiful!
Passing a List to a Function
Recall in U1-10 we talked about passing arguments to functions. Until now, when we have passed an int, float, str, or bool variable, what the Python interpreter actually does is make a copy of each argument value and assign it to an associated parameter variable in the function. This kind of argument passing is called pass-by-value (or pass-by-copy). The reason that these values are passed-by-value is that they all are immutable types.
Because lists can contain much more data than a typical integer, float, string, or Boolean variable, they are passed to a function using an approach called pass-by-reference. With pass-by-reference, only a copy of the reference to the list object is passed into the function, the data stored in the list is not duplicated. This means that the argument variable and the parameter variable will be referring to the same list object in memory. All mutable objects, like lists, are passed by reference.
The reason for this is one of efficiency. If the list is very large, it would be very inefficient in terms of memory usage to make a second complete copy of it. The most important thing to realize with pass-by-reference is that if you change the list that the parameter variable is referring to it also changes the list that the original argument variable was referring to (since they both refer to the same list in memory).
Consider the following example:
1 2 3 4 5 6 7 8 |
def change_list(numbers): numbers[1] = 99 nums = [ 1, 2, 3, 4 ] print(nums) # displays: [1, 2, 3, 4] change_list(nums) print(nums) # displays: [1, 99, 3, 4] |
There is nothing wrong with changing a list referred to by a parameter variable in a function, as long as you are doing it intentionally. Just be careful not to accidentally change a list parameter or your function will cause some strange side effects.
List Methods
Lists have numerous methods that allow you to add elements, remove elements, change the ordering of elements, and so forth. Below is a summary of these methods:
Note: Because lists are mutable, the methods above actually change the original list they are being called on. They do not return a copy of the list (as many of the string methods did). All of the methods above simply return None.
Here is code to demonstrate these methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
letters = [ 'a', 'b', 'd' ] print("Original List:", letters) letters.append('x') print("After append('x'):", letters) letters.insert(2, 'c') print("After insert(2, 'c'):", letters) letters.remove('b') print("After remove('b'):", letters) letters.reverse() print("After reverse():", letters) |
And the output:
Original List: [‘a’, ‘b’, ‘d’]
After append(‘x’): [‘a’, ‘b’, ‘d’, ‘x’]
After insert(2, ‘c’): [‘a’, ‘b’, ‘c’, ‘d’, ‘x’]
After remove(‘b’): [‘a’, ‘c’, ‘d’, ‘x’]
After reverse(): [‘x’, ‘d’, ‘c’, ‘a’]
You Try!
-
- Start a new page in your Learning Journal titled “3-6 Lists“. Carefully read the notes above and in your own words summarize the key ideas from each section.
- Explain the differences between an immutable and mutable data type.
- What is the best way to find the number of elements in a list?
- What will happen if you try to add or change an element in a list using an invalid index?
- Try to figure out what the following code will display, then check your hypothesis using the Python interpreter.
1234numbers = [ 1, 2, 3, 4, 5 ]numbers[2] = 99my_list = numbers[1:3]print(my_list) - A hacker-level challenge (from UofT): Your classmate claims to have written a function that replaces each value in a list with twice the preceding value (and the first value with 0). For example, if the list [1, 3, 7, 11] is passed as a parameter, the function is supposed to return [0, 2, 6, 14] — Note: 22 is not part of the output. Here’s the code:
1234567def double_preceeding(values):if (values != []):temp = values[0]values[0] = 0for i in range(1, len(values)):values[i] = 2 * temptemp = values[i]
Analyse this function and rewrite it so that it works as intended. - Another hacker-level challenge from UofT: Consider the following function:
123456def remove_negs(num_list):for item in num_list:if item < 0:num_list.remove(item)return num_list
When remove_negs([1, 2, 3, -3, 6, -1, -3, 1]) is executed, the result is: [1, 2, 3, 6, -3, 1]. Your classmate thinks what is happening is that if there are two negative numbers in a row (e.g., -1,-3) that the second number will not get removed. Explain what the problem is. Rewrite the function so that it works as intended.