2018.02.27

# Lists vs Strings

Lists are similar to strings, but more flexible in two ways:

1. The elements of a list can be of any type, not just characters.

2. Lists are mutable: they can be changed after being created.

To write a list, use brackets and commas:

``````[1 , 2 , 3]  #  a list of three integers
[]           #  an empty list``````

# Working with Lists

• The `len` function is overloaded to work on lists:

``len ([1 , 2 , 3]) == 3``
• Lists support indexing (`_[_]`) and slicing (`_[_:_]`) just like strings:

``````[1 , 2 , 3]      ==  1
[1 , 2 , 3] [1 : ]  ==  [2 , 3]``````
• The operators for concatenation (`_+_`) and repetition (`_*_`) are overloaded to work on lists:

``````[1 , 2 , 3] + [4 , 5]  ==  [1 , 2 , 3 , 4 , 5]
[1 , 2 , 3] * 2        ==  [1 , 2 , 3 , 1 , 2 , 3]``````
• `for` loops work on lists too, assigning the loop variable to each list element in turn:

``````for number in [1 , 2 , 3] :
print (str(number))``````
• The `list` coercion function “explodes” a string into the list of its characters:

``list ('hello!')  ==  ['h' , 'e' , 'l' , 'l' , 'o' , '!']``

# Homogeneous Lists

• A homogeneous list is one where all of its elements have the same type.

• In this course all lists will be homogeneous. (We’ll meet some non-homogeneous data structures later.)

• To Python, the type of any list is just “`list`”:

``type ([1 , 2 , 3]) == type (['a' , 'b'])  #  evaluates to True``
• But we usually need to know the type of a list’s elements as well:

``````def my_sum (nums) :
"""
signature:  list (int) -> int
returns the sum of the numbers in the list, or 0 if empty
"""
acc  =  0
for num in nums :
acc  +=  num
return acc``````
• Signatures for homogeneous lists should specify the element type.

# Index Assignment

Unlike any type we’ve seen so far, the elements of `list` type can be changed after being created.

The element located at a list index can be changed using index assignment:

``<list> [<index>]  =  <new_element>``
``xs  =  list ('hello!')`` ``````xs   =  'o'
xs   =  'a'`````` # Slice Assignment

A slice can be cut out of a list and a new list spliced into its place using slice assignment:

``<list> [<start_index> : <stop_index>]  =  <splice_list>``

The list spliced in need not be the same length as the slice cut out.

``````xs  =  list ('holla!')
xs [1 : 5]  =  ['i' , 'y' , 'a']`````` • If the list that is spliced in has length `0`, then this just cuts out a slice.

• If the slice that is cut out has length `0`, then this just splices in a list.

# Aliasing

• Until we met lists, all the Python types we knew were immutable.

• For example, it doesn’t matter whether or not two string variables refer to the same copy of `'hello'`,

because we can’t do index or slice assignment on strings to change them.

• But with lists, it does matter.

• Putting a list reference on the right side of an assignment causes the left side to refer to the same list.

This is called aliasing.

``````x  =  [1 , 2 , 3]
y  =  x`````` # Aliasing and Mutability

``````x  =  [1 , 2 , 3]
y  =  x
z  =  [1 , 2 , 3]`````` If we make changes to a list using one of its aliases then those changes are visible from any other alias too.

``y   =  5`` ``x  == 5  and  z  == 2    #  evaluates to True``

# Detecting Aliasing

• Use the “`_is_`” or “`_is not_`” operators to see whether two references alias the same value:

``````<ref_1> is <ref_2>
<ref_1> is not <ref_2>    #  same as: not (<ref_1> is <ref_2>)``````
• The “`_==_`” and “`_!=_`” operators compare list equality content-wise, not reference-wise, so they can’t detect aliasing.

``````x = [1 , 2 , 3]
y = x
z = [1 , 2 , 3]`````` ``````y == x  and  y is x        #  evaluates to True
z == x  and  z is not x    #  evaluates to True``````

# Mutator Functions

• When a list is passed to a function as an argument, the corresponding parameter becomes an alias to that list.

• When a function makes changes to an argument list, that is a type of effect, called a mutation (or “modification”).

• It’s important to understand whether a function acts by returning a new list or by mutating an argument list. Compare:

``````def double_effect_free (ns) :
"""
signature: list (int) -> list (int)
returns a list containing the doubles of the input list elements
"""
acc  =  []
for n in ns :
acc  +=  [n * 2]
return acc``````
``````def double_by_mutation (ns) :
"""
signature: list (int) -> NoneType
effect: doubles every list element in place
"""
i  =  0
while i < len (ns) :
ns [i]  *=  2        #  i.e.  ns [i]  =  ns [i] * 2
i  +=  1``````

# List Methods

The `list` namespace contains functions/methods that act on lists, often by mutation. For example:

``list.insert (<list> , <index> , <item>)  #  modifies the list by inserting the new item at the specified index``
``````evens  =  [2 , 6]
evens.insert (1 , 4)
evens == [2 , 4 , 6]``````
``list.append (<list> , <item>)  #  modifies the list by adding the new element to the end``
``````evens.append (8)
evens == [2 , 4 , 6 , 8]``````
``list.extend (<list> , <other_list>)  #  modifies the list by appending all the elements of another list``
``````evens.extend ([10 , 12])
evens == [2 , 4 , 6 , 8 , 10 , 12]``````
``list.pop (<list> , <index>)  #  modifies the list by removing the element at the specified index and returning it``
``````ten  =  evens.pop (4)
evens == [2 , 4 , 6 , 8 , 12] and ten == 10``````

All of these can be easily written in just a few lines using slice assignment.

# Generic List Operations

• Usually, to process a list we need to know the type of its elements (e.g. we can do arithmetic only with numbers).

• But some operations work for any kind of list because they rely on only the list structure, not the element type.

Functions that act this way are called type-generic:

``````def my_reversed (xs) :
"""
signature:  any a . list (a) -> list (a)
returns the reversed version of a list of any type
"""
acc  =  []
for x in xs :
acc.insert (0 , x)
return acc``````
• This function doesn’t examine the elements of the list in any way, that’s how it’s able to treat them generically.

• The “`a`” in “`any a`” in the signature is a type parameter that can be assigned any type.

Python has a built-in function called “`reversed`” that does the same thing, except that it returns an iterator.

# Digression: Iterators

An iterator is any type that can be iterated over with a `for` loop. `list`s are examples of iterators.

It is a quirk of Python that many built-in functions that we would expect to return `list`s actually return other `iterator`s instead.

For example, the `range` function has signature: `int , int -> iterator (int)`

``range (0 , 10)``

All we will care about `iterator`s in this course is that they can be converted to `list`s.

``list (range (0 , 10))``

If indexing into an object that you think is a list causes an error, it may actually be a different iterator instead. Try converting it to a list with the `list` coercion function.

# Mapping over a List

• One common use of `for` loops is to process each element of a list in the same way:

``````def double (num) :
# signature:  int -> int
return 2 * num``````
``````def double_all (nums) :
# signature:  list (int) -> list (int)
acc  =  []
for num in nums :
acc.append (double (num))
return acc``````
• This pattern is called mapping: # Higher-Order Mapping

• Python has a built-in function called “`map`” that does the same thing:

``````def map_double (nums) :
# signature:  list (int) -> list (int)
return list (map (double , nums))``````
• Note that the first argument to the `map` function is another function!

``map  #  signature: any a b . function (a -> b) , iterator (a) -> iterator (b)``
• This makes `map` a higher-order function: a function that manipulates other functions.

• `map` takes the argument function and applies it to each element of the argument list.

• Higher-order functions like `map` let you work at a higher level of abstraction

and free you from having to write repetitive bits of code.

# Filtering a List

• Another common use of `for` loops is to select from a list the elements that satisfy some predicate:

``````def is_even (n) :
# signature:  int -> bool
return n % 2 == 0``````
``````def just_evens (ns) :
# signature:  list (int) -> list (int)
acc  =  []
for n in ns :
if is_even (n) :
acc.append (n)
return acc``````
• This pattern is called filtering a list. Python has a higher-order function called “`filter`” to do this:

``````def filter_evens (ns) :
# signature:  list (int) -> list (int)
return list (filter (is_even , ns))``````
• The first argument to `filter` is a predicate (i.e. a `bool`-valued function):

``filter  #  signature: any a . function (a -> bool) , iterator (a) -> iterator (a)``

# To Do This Week:

• Reading:

• chapter 10: lists (11 pages)
• Come to lab on Thursday.

• Complete homework 5.

• Review study guide for midterm 1