14. Aliases & List Trivia

  • The following code should look familiar 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 the below example should follow naturally

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 my_list with arbitrary contents.

  2. Create an alias of my_list called my_list_alias.

  3. Create a copy of my_list called my_list_copy.

Verify that my_list_alias is an alias and my_list_copy is a 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]
  • Even though we never used a_list inside the function, the list it references was changed through the alias some_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

Python provides these, but someone still had to write them.

  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 Topic