[10, 20, 30, 40]
['dog', 'fish', 'bird']

"""
Explanation: Chapter 10: Lists

Contents
- A list is a sequence
- Lists are mutable
- Traversing a list
- List operations
- List slices
- List methods
- Map, filter and reduce
- Deleting elements
- Lists and strings
- Objects and values
- Aliasing
- List arguments
- Debugging
- Exercises

This notebook is based on "Think Python, 2Ed" by Allen B. Downey <br>
https://greenteapress.com/wp/think-python-2e/

A list is a sequence

A list is a sequence of values (like a string)
The values are referred to as elements or items
The simplest way to create a list is using square brackets
End of explanation
"""


['bob', 3.14, 42, ['sam', 55]]

"""
Explanation: The elements of a list don't have to be the same data type in Python (in other languages they do)
However, if you want a sequence with different data types there are better alternatives
End of explanation
"""


cheeses = ['Chedder', 'Pepper Jack', 'Queso Fresca']
grades = [99, 84, 91]
empty = []

"""
Explanation: As you can see, lists can be nested
A list containing no elements is an empty list
An empty list is created using empty brackets []
You can assign a list to a variable
End of explanation
"""


cheeses = ['Chedder', 'Pepper Jack', 'Queso Fresca']
cheeses[0] = 'Gouda'
print( cheeses )

"""
Explanation: Lists are mutable

Unlike strings, lists are mutable
This means we can change them
End of explanation
"""


for cheese in cheeses:
    print( cheese )

"""
Explanation: Note that like strings, indices start at 0
In fact, all the things you can use for indices and use indices for in strings, you can do in lists
This includes the in operator
One way to think of the relationship between indices and elements is that it is a mapping
Each index maps to one of the elements
Much like a word maps to a definition in a dictionary
TODO State diagram for some lists

Traversing a list

The most common way to traverse a list is with a for loop
It is the same approach we used with strings
End of explanation
"""


numbers = [1, 2, 3]

for i in range( len( numbers ) ):
    numbers[i] = numbers[i] * 2

print( numbers )

"""
Explanation: However, if you need the index, you need to use the range and len functions
For example, you may want to update the values in the list as you traverse it
End of explanation
"""


nested_list = ['spam', 1, [ 'Brie', 'Roquefort' , 'Pol le Veq'], [1, 2, 3] ]
len( nested_list )

"""
Explanation: A for loop over an empty list never executes the body of the loop
If you have a nested list, the nested list still counts as a single element
End of explanation
"""


a = [ 1, 2, 3 ]
b = [ 4, 5, 6 ]
c = a + b
print( c )

"""
Explanation: List operations

The + operator concatenates lists
This means that it links them together into a single list
End of explanation
"""


d = [ 0 ] * 4
print( d )

e = [ 1, 2, 3 ] * 3
print( e )

"""
Explanation: Similarly, the * operator repeats a list a specified number of times
End of explanation
"""


f = [ 'a', 'b', 'c', 'd', 'e', 'f' ]
print( f[1:3] )
print( f[:4] )
print( f[3:] )

"""
Explanation: List slices

The slice operator also works on lists:
End of explanation
"""


f[1:3] = [ 'x', 'y' ]
print( f )

"""
Explanation: Just as with strings, if you omit the first index the slice starts at the beginning
If you omit the second, the slice goes to the end
If you omit both, the slice is th entire list
Since lists are mutable, sometimes it is important to make a copy of a list before you change it
You can use a slice to update multiple elements
End of explanation
"""


list1 = [ 'a', 'b', 'c' ]
list1.append( 'd' )
print( list1 )

"""
Explanation: List methods

append will add a new element to the end of a list
End of explanation
"""


list2 = [ 'a', 'b', 'c' ]
list3 = [ 'd', 'e', ]
list2.extend( list3 )

print( list2 )
print( list3 )

"""
Explanation: extend takes a list as an argument and appends all of the elements
End of explanation
"""


list4 = [ 'd', 'c', 'e', 'a', 'b' ]
list4.sort()
print( list4 )

"""
Explanation: Note that list3 is unmodified
sort arranges the elements of a list from low to high
End of explanation
"""


def add_all( a_list ):
    total = 0

    for value in a_list:
        total += value

    return total

"""
Explanation: All of these list methods are void
This means that they modify the list argument and return None

Map, filter and reduce

To add up all the numbers in a list, use a loop
End of explanation
"""


list_of_numbers = [ 1, 2, 3 ]
sum( list_of_numbers )

"""
Explanation: As we have seen before, the += operator updates a variable
It is referred to as an augmented assignment statement
In the code, total is used in the loop as an accumulator
Python provides the sum function to sum the elements of a list
End of explanation
"""


def capitalize_all( a_list ):
    result = []
    for a_string in a_list:
        result.append( a_string.capitalize() )
    return result

words = [ 'life', 'the', 'universe', 'and', 'everything' ]
capitalized_words = capitalize_all( words )
print( capitalized_words )

"""
Explanation: A function that combines a sequence of elements into a single value is sometimes called a reduce function
Sometimes you want to build a new list from an existing list
End of explanation
"""


def only_upper( a_list ):
    result = []
    for a_string in a_list:
        if( a_string.isupper() ):
            result.append( a_string )
    return result

words = [ 'LIFE', 'The', 'uNiverse', 'AND', 'everything' ]
some_words = only_upper( words )
print( some_words )

"""
Explanation: This is sometimes called a map function since it "maps" a function onto each of the elements in a sequence
Note that a map is more commonly used to refer to a specific data structure (e.g., a dictionary)
Another common operation is to filter elements from one list to another
End of explanation
"""


a_list = [ 'a', 'b', 'c' ]
value = a_list.pop( 1 )
print( a_list )
print( value )

"""
Explanation: Deleting elements

The pop function can be used to delete an element from a list if you know the index
End of explanation
"""


a_list = [ 'a', 'b', 'c' ]
del a_list[1]
print( a_list )

"""
Explanation: Note that the function returns the deleted element
If you don’t specify an index, it operates on the last element
If you don’t need the deleted value, use the del operator
End of explanation
"""


a_list = [ 'a', 'b', 'c', 'd', 'e', 'f' ]
del a_list[1:5]
print( a_list )

"""
Explanation: del can also be used with a slice index to remove more than one element
End of explanation
"""


a_list = [ 'a', 'b', 'c', 'a', 'b', 'c' ]
a_list.remove( 'b' )
print( a_list )

"""
Explanation: If you know the element you want to remove, but not the index, use the remove operator
Note that it only removes the first matching element if there are duplicates
End of explanation
"""


a_string = 'spam'
a_list = list( a_string )
print( a_list )

"""
Explanation: Lists and strings

A list of characters is not the same as a string
To convert a string to a list, use the list operator
End of explanation
"""


a_string = 'pining for the fjords'
a_list = a_string.split()
print( a_list )

"""
Explanation: Since it is an operator, don’t use list as a variable name
Don’t use l either since it looks like a 1
Use a_list or something more descriptive detailing a value in your algorithm
If you want to split a string into words and not letters, use split
End of explanation
"""


a_string = 'spam-spam-spam'
delimiter = '-'
a_string.split( delimiter )

"""
Explanation: By default, the delimiter is a space
You can change that by passing it as an argument
End of explanation
"""


a_list = [ 'pining', 'for', 'the', 'fjords' ]
delimiter = ' '
delimiter.join( a_list )

"""
Explanation: join is the inverse of split
Since it is a string method, you have to invoke it on a string
In this case, invoke it on the delimiter
End of explanation
"""


a = 'banana'
b = 'banana'
a is b

"""
Explanation: Objects and values

To check whether two variables refer to the same object, use the is operator
End of explanation
"""


a = [ 1, 2, 3 ]
b = [ 1, 2, 3 ]
a is b

"""
Explanation: Note that this is different from the values of the two objects being equivalent
End of explanation
"""


a = [ 1, 2, 3 ]
b = a
b[0] = 17
print( a )

"""
Explanation: In the first example, Python only create one string object
In the second, Python created two lists that are equivalent, but not identical
Why the difference?

Aliasing

A variable doesn’t actually hold an object, it has a reference to it
When two variables refer to the same object, it is called aliasing
If an object is mutable, changes made to one affect the other
End of explanation
"""


def delete_head( a_list )
    del a_list[0]

letters = [ 'a', 'b', 'c' ]
delete_head( letters )
print( letters )

"""
Explanation: Be careful because this can cause problems if you aren’t paying attention
For immutable objects (like strings), it isn’t a problem

List arguments

When you pass a list as an argument to a function, it gets a reference to the list
What does this mean?
If the list modifies the list, it is the same list as in the calling function
End of explanation
"""


list1 = [ 1, 2 ]
list2 = list1.append( 3 )
print( list1 )
print( list2 )

"""
Explanation: See the stack diagram on pg. 97 of the textbook
Make sure you know if the function you are using modifies the list argument or returns a new list
End of explanation
"""


def cumulative_sum( a_list ):
    # INSERT YOUR CODE HERE
    return []

numbers = [ 1, 2, 3 ]
result = cumulative_sum( numbers )
# The answer should be:
# [ 1, 3, 6 ]

"""
Explanation: Debugging

Be careful of common pitfalls when using lists and other mutable objects:
Most list methods modify the argument and return None. This is the opposite of string methods.
Pick an idiom (or way of doing things) and stick with it
Make copies to avoid aliasing and accidental modifications

Exercises

Write a function called cumulative_sum that takes a list of numbers and returns the cumulative sum; that is, a new list where the $i$-th element is the sum of the furst $i$ elements from the list.
End of explanation
"""


def chop( a_list ):
    # INSERT YOUR CODE HERE
    print( 'Remove this line' ) # Jupyter needs a statement to compile

numbers = [ 1, 2, 3, 4 ]
chop( numbers )
print( numbers )
# Should print: [ 2, 3 ]

"""
Explanation: Write a function called chop that takes a list and modifies it by removing the first and last element.  The function should return None.
End of explanation
"""


