14. Aliases & List Trivia

  • The following code should be simple to understand at this stage

1a = 5
2b = a
3print(a, b)     # Results in 5 5
4
5b = 7
6print(a, b)     # Results in 5 7 --- a is left unchanged
  • And similarly, the below example should not surprise you

1a = [0, 1, 2]
2b = a           # b is an "alias" for a
3print(a, b)     # Results in [0, 1, 2] [0, 1, 2]
4
5b = [5, 6, 7]   # Change what b references
6print(a, b)     # Results in [0, 1, 2] [5, 6, 7] --- a is left unchanged
  • However, the following may throw you off

1a = [0, 1, 2]
2b = a           # b is an "alias" for a
3print(a, b)     # Results in [0, 1, 2] [0, 1, 2]
4
5b[1] = 99       # Change index 1 of the list b references
6print(a, b)     # Results in [0, 99, 2] [0, 99, 2]
  • Remember, a and b are both referencing the same list

    • They are aliases

    • There is only one list, but we have two references to that one list

  • Regardless of the variable used to modify the single list, it’s the list that is being altered

  • If you expected the line b = a to make a full copy of the list referenced by a, then this will seem strange

  • But the line b = a does not make a copy of the list — it makes a copy of the reference to the list

Warning

This idea of references and aliases goes well beyond lists and will come up more as we progress through the course. It is something you will get used to with practice, but be aware that mixing up references is a very common error even experienced programmers run into.

  • If you do actually want to make a copy of a list, there are a few ways to do it

    • Lists have a copy method that returns a copy new_list = some_list.copy()

    • It is also possible to slice the list to produce a copy new_list = some_list[:]

Activity

  1. Create a list l with arbitrary contents.

  2. Create an alias of l called l_alias.

  3. Create a copy of l called l_copy.

Convince yourself that you did in fact make an alias with l_alias and a copy with l_copy,

14.1. Functions and Aliasing

  • When a list is given to a function, the parameter will get a reference to the list and not a copy of the list

    • The parameter within the function will be an alias

1def add_to_list(some_list, value):
2    some_list.append(value)
3
4a_list = ['a', 'b', 'c']
5add_to_list(a_list, 99)
6print(a_list)   # Results in ['a', 'b', 'c', 99]
  • In the above example, although never access through a_list, the list a_list references is altered through the alias some_list within the function add_to_list

14.1.1. Side Effects & Pure Functions

  • add_to_list is an example of a function that has a side effect

    • The function modified the list that was passed by reference

    • The term side effect comes from our mathematical expectation of a function

      • A function maps some parameters on to a value

      • If I give you the function \(f(x, y, z)= x + y - z\) and ask you to evaluate \(f(1, 2, 3)\), you don’t expect the values of \(x\), \(y\), and \(z\) to change

  • We can write a different version of the function that has no side effect

    • Functions without side effects are called pure functions

1def add_to_list_pure(some_list, value):
2    new_list = some_list.copy()
3    new_list.append(value)
4    return new_list
5
6a_list = ['a', 'b', 'c']
7other_list = add_to_list_pure(a_list, 99)
8print(a_list)           # Results in ['a', 'b', 'c']
9print(other_list)       # Results in ['a', 'b', 'c', 99]
  • In the new function add_to_list_pure, the function makes a copy of the list passed by reference and made changes to the copy

  • The new list was returned

    • In the end, the original list’s data was left alone

  • There are nice theoretical and practical benefits to keeping functions pure

  • But that does not mean that non-pure functions are intrinsically bad

    • Sometimes it’s just a lot easier to achieve something with side effects

14.2. List Trivia

  • We can find the length of a list

1some_list = [10, 11, 12]
2print(len(some_list))       # Results in 3
  • We can have empty lists

1empty_list = []
2print(empty_list)           # Results in []
3print(type(empty_list))     # Results in <class 'list'>
4print(len(empty_list))      # Results in 0
  • We can have lists of lists

1some_nested_lists = [[0, 1, 2], ['a', 'b', 'c']]
2print(some_nested_lists[1])     # Results in ['a', 'b', 'c']
3print(some_nested_lists[1][0])  # Results in 'a'
  • We can append to a list

1some_list = [10, 11, 12]
2some_list.append(99)
3print(some_list)       # Results in [10, 11, 12, 99]
  • We can concatenate lists with + to create a new list

    • The original lists are left unchanged

1list_a = [0, 1, 2]
2list_b = ['a', 'b', 'c']
3list_c = list_a + list_b
4print(list_a)               # Results in [0, 1, 2]
5print(list_b)               # Results in ['a', 'b', 'c']
6print(list_c)               # Results in [0, 1, 2, 'a', 'b', 'c']
  • We can repeat a list with *

1some_list = [10, 11, 12]
2print(some_list * 3)       # Results in [10, 11, 12, 10, 11, 12, 10, 11, 12]

Activity

Python has some built in functions that we can use on lists:

  • min

  • max

  • sum

However, just because Python provides these functions, someone still had to write these functions.

  1. Without using the built in sum, write your own function my_sum to add up the contents of a list.

  2. How different do you think your algorithm is compared to the one Python gave you?

  3. If you had a list of length \(10\), how many things does your function need to add together?

  4. What if your list was length \(10,000\)?

14.3. For Next Class