20130124

Python provides a number of built-in data structures.  The most commonly used one is the list.

A Python list is an ordered sequence of values.  Each value (or element) in a list can be referenced by its position in the list.  The first position in the list (for no good reason) is position 0.

We can create a Python list in several different ways - the simplest is to specify the values, as in

list_1 = [4,67,23,89]

Each location in a list can be treated as an independent variable, as in

list_1[3] = 47

Printing list_1 at this point would show

[4,67,23,47]

In fact, since each element of a list can be treated as an independent variable, the elements of a list do not have to be the same type of value.  For example

list_1[2] = "Greetings, citizen"
print list_1

gives

[4,67,"Greetings, citizen",47]

In fact, the elements of a list can even be other lists.

list_1[0] = ["a",14,"b"]
print list_1

gives

[["a",14,"b"],67,"Greetings, citizen",47]



Lists differ from simple variables in a crucial way.  With a simple variable  the variable name is associated with the location in memory where the value is stored..  However, with a list variable the variable name is associated with a location on memory where an address is stored - the address where the first value of the list is stored.  In computing, we call this indirect addressing.  It is common to refer to the name of the list as a pointer to the list.

This becomes important when we consider code like this:

x = 9        # stores 9 in memory and calls the location x
y = x        # copies 9 to another location in memory and calls this location y
x = 4        # stores 4 in the memory location called x
print y      # prints 9

list_1 = [4,7,2,8,3]        # creates a list containing 4,7,2,9,3.  Stores the location of this list in a memory location
                            #    and calls this location list_1
list_2 = list_1             # copies the contents of the location called list_1 to a new location and calls this
                            #     location list_2
                            # at this point, list_1 and list_2 are both pointers to the same list in memory
list_1[2] = 999             # changes the value stored in position 2 of list_1
print list_2                # prints [4,7,999,8,3]

As you can see, changing one element of list_1 seems to magically change an element of list_2 as well.  Of course what is really happening is that list_1 and list_2 are both pointing to the same list in memory.  This is called aliasing and it is a frequent cause of errors in our programs.

To create a new list which is a copy of an existing list, rather than becoming an alias, we use what Python calls a slice.

list_1 = [4,7,2,8,3]
list_2 = list_1[:]            # creates a copy of the contents of list_1, stores the copy in memory, stores the address
                              # of the copy in a location, and calls that location list_2
list_1[2] = 999               # changes the value stored in position 2 of list_1
print list_2                  # prints [4,7,2,8,3]

Now changing an element of list_1 has no effect on list_2 because we made a completely separate copy of the list using the slice [:]

We can use slicing to make a copy of part of a list.  For example

list_1 = [4,7,5,2,8,3,6,1,9]
list_2 = list_1[2:6]          # copies positions 2, 3, 4, and 5 of list_1
print list_2                  # prints [5,2,8,3]

Notice that the copied slice begins in position 2 and ends in position 5 ... one position before the end position specified in the [2:6] ... this is consistent with other aspects of Python.  For example, range(10) produces the list [0,1,2,3,4,5,6,7,8,9] ... the largest value in the range is one less than the value specified.

If we want a slice to start in position 0, we can leave out the 0 in the slice instruction.  For example

list_2 = list_1[:5]        # is exactly the same as list_2 = list_1[0:5]

If we want a slice to go right to the end of the original list, we can leave out the value after the :  .  For example

list_2 = list_1[3:]        # is exactly the same as list_2 = list_1[3:9]

We can use negative numbers in the slice indices - these represent positions counting from the end of the list.  A -1 represents the last position, -2 the second last, etc.  So if we want a slice that contains the last three elements of list-1 we can do this.

list_2 = list_1[-3:]

This is very useful if we want to make a copy of the last few elements of the list and we don't know how long the list is.  

Actually there is a way to find the length of a list, using the built-in len() function.

x = len(list_1)        # the value assigned to x is the number of elements in list_1

We could use len(list_1) in a slice instruction, as in

list_2 = list_1[-3:len(list_1)]        # this does exactly the same thing as     list_2 = list_1[-3:]

These are equivalent, but you should probably use list_2 = list_1[-3:]   because it is easier to read.