# Class Notes: 1d Lists

### The Basics Creating Lists [Pre-reading] Built-in Functions with Lists [Pre-reading] Comparing Lists [Pre-reading] Accessing Elements (Indexing and Slicing) [Pre-reading] Modifying Lists [Pre-reading] Lists are Mutable List Mutability and Aliasing [Pre-reading] Copying Lists [Pre-reading] Destructive and Non-destructive Functions Coding with Lists Finding Elements Adding Elements Removing Elements Looping Over Lists List Methods: Sorting and Reversing Summary: Destructive and Non-destructive Additional Information Tuples (Immutable Lists) List Comprehensions Converting Between Lists and Strings Worked Examples Using Lists

• Creating lists with multiple elements
evens = [2, 4, 6, 8] pets = ['cat', 'dog', 'pig'] # We can even mix data types within a list petInfo = ['cat', 'Linus', 14, 15.6, True] # Can use print and type with lists print(petInfo) print(type(petInfo)) # The built-in function for length is really useful with lists n = len(petInfo) print(n)

• Lists with one element
a = ["hello"] b =  print(type(a), len(a), a) print(type(b), len(b), b)

• Empty List
print("Two standard ways to create an empty list:") a = [] b = list() print(type(a), len(a), a) print(type(b), len(b), b)

• Other ways to create lists
# Create a new list with n 0s n = 10 a =  * n # Create a list by forcing the expansion of a range object b = list(range(n)) print(type(a), len(a), a) print(type(b), len(b), b)

### Built-in Functions with Lists [Pre-reading]

a = [2, 3, 5, 2] print("a = ", a) print("len =", len(a)) print("min =", min(a)) print("max =", max(a)) print("sum =", sum(a))

To help us understand how to compare lists, let's first discuss a few things about Python objects. We've actually seen quite a few examples of objects in Python, including ints, floats, strings, etc. In fact, everything in Python is an object.

Python objects always have three pieces:
• a value (which we can usually see via the `print` function),
• a type (which we can see via the `type` function), and
• an id (which we can see via the `id` function)

You can think of the object id as kind of a serial number for each object. This id will help us to determine if two variable names represent the exact same list or not. See the difference between `==` and `is` below.

# Create some lists a = [10, 20, 30] b = a c = [10, 20, 30] # Is c the same as a and b?? d = [10, 20, 33] # d is totally different print("a:", a) print("b:", b) print("c:", c) print("d:", d) print() # Variable names a and b both point to the exact same list object # We can confirm this by printing the id for each variable name print("id(a):", id(a)) print("id(b):", id(b)) print("id(c):", id(c)) print("id(d):", id(d)) print() # c is a list that has the same contents as a (and b) but is actually a # different object # Use the visualize button to help see this difference in pythontutor # Operator `list1 == list2` checks to see if the two objects are # "equal", specifically: # 1) both objects are lists # 2) both lists have the same length # 3) the corresponding elements in each list are equal # Operator `list1 is list2` checks to see if the two objects are # actually the same object, specifically: both objects have the same id print("a == b:", (a == b)) print("a == c:", (a == c)) print("a == d:", (a == d)) print() print("a is b:", (a is b)) print("a is c:", (a is c)) print("a is d:", (a is d)) print() print("a != b:", (a != b)) print("a != c:", (a != c)) print("a != d:", (a != d)) print() print("a is not b:", (a is not b)) print("a is not c:", (a is not c)) print("a is not d:", (a is not d))

### Accessing Elements (Indexing and Slicing) [Pre-reading]

# Indexing and slicing for lists works the same way as it did for strings! a = [2, 3, 5, 7, 11, 13] print("a =", a) # Access non-negative indexes print("a =", a) print("a =", a) # Access negative indexes print("a[-1] =", a[-1]) print("a[-3] =", a[-3]) # Access slices a[start:end:step] print("a[0:2] =", a[0:2]) print("a[1:4] =", a[1:4]) print("a[1:6:2] =", a[1:6:2])

dna = ['G', 'A', 'T', 'T', 'A', 'C', 'A'] print(id(dna)) print(dna) dna = 'C' print(dna) dna = 'G' print(dna) dna[-2] = 'G' print(dna) print(id(dna)) # Note the object id stayed the same. We "mutated" the list object rather than # making new list objects

Other ways to modify (mutate) a list
a = [10, 20, 30, 20] print(id(a)) print(a) a.append(10) # Add element to the end print(a) a.insert(2, 25) # Insert 25 into index 2 (bumping others to the right) print(a) a.pop() # Remove element from the end print(a) a.pop(3) # Remove element at index 3 print(a) a.remove(20) # Remove (first occurrence of) element with value 20 print(a) print(id(a)) # Same id as before we did any of these mutations # Append, insert, pop and remove are all examples of "destructive" methods, i.e. # methods that change the contents of the list *without* creating a new list

### List Mutability and Aliasing [Pre-reading]

Unlike strings, lists are mutable. This means that they can be changed, without creating a new list.
This also forces us to better understand aliases, when two variables reference the same value. Aliases are only interesting (and challenging) for mutable values like lists.
Note: it will be especially helpful to use the Visualize feature in the following examples.

• Example:
# Create a list a = [ 2, 3, 5, 7 ] # Create an alias to the list b = a # We now have two references (aliases) to the SAME list a = 42 b = 99 print(a) print(b)

• Function Parameters are Aliases:
def f(a): a = 42 a = [2, 3, 5, 7] f(a) print(a) # Note that the parameter alias can still be broken by re-assigning the variable a = [3, 2, 1] def foo(a): a = 1 a = [5, 2, 0] # we break the alias here! a = 4 foo(a) print(a)

• Another Example:
# Create a list a = [ 2, 3, 5, 7 ] # Create an alias to the list b = a # Create a different list with the same elements c = [ 2, 3, 5, 7 ] # a and b are references (aliases) to the SAME list # c is a reference to a different but EQUAL list print("initially:") print(" a==b :", a==b) print(" a==c :", a==c) print(" a is b:", a is b) # the is operation tells if two values are aliases print(" a is c:", a is c) # Now changes to a also change b (the SAME list) but not c (a different list) a = 42 print("After changing a to 42") print(" a=", a) print(" b=", b) print(" c=", c) print(" a==b :", a==b) print(" a==c :", a==c) print(" a is b:", a is b) print(" a is c:", a is c)

• Copy vs Alias
# Because of aliasing, we have to be careful if we share a reference # to a list in the same way we might for number or a string, # by simply setting b = a, like so: import copy # Create a list a = [ 2, 3 ] # Try to copy it b = a # Error! Not a copy, but an alias c = copy.copy(a) # Ok # At first, things seem ok print("At first...") print(" a =", a) print(" b =", b) print(" c =", c) # Now modify a a = 42 print("But after a = 42") print(" a =", a) print(" b =", b) print(" c =", c)

• Other ways to copy
import copy a = [2, 3] b = copy.copy(a) c = a[:] d = a + [ ] e = list(a) a = 42 print(a, b, c, d, e)

### Destructive and Non-destructive Functions

Because lists are mutable, we can change them in two ways:
destructively (which modifies the original value directly), and
non-destructively (which creates a new list and does not modify the original value).
This also affects how we write functions that use lists.

• Destructive functions
# A destructive function is written to directly change the provided list # It does not need to return anything, as the caller can access the original list def fill(a, value): for i in range(len(a)): a[i] = value a = [1, 2, 3, 4, 5] print("At first, a =", a) fill(a, 42) print("After fill(a, 42), a =", a)

• Non-destructive function
import copy ## First, a quick primer on modifying lists ## ## We'll talk about these more in a bit ## a = [1, 2, 3, 4] # .remove() DESTRUCTIVELY removes the given value from the list a.remove(2) print(a) # [1, 3, 4] # .append() DESTRUCTIVELY adds the given value to the end of the list a.append(70) print(a) # [1, 3, 4, 70] ## Now, on to NON-DESTRUCTIVE functions! ## def destructiveRemoveAll(a, value): while (value in a): a.remove(value) def nonDestructiveRemoveAll(a, value): # Typically, we write non-destructive functions by building a new list # instead of changing the original result = [] for element in a: if (element != value): result.append(element) return result # non-destructive functions still need to return! def alternateNonDestructiveRemoveAll(a, value): # We can write the same function by breaking the alias, # then using the destructive approach a = copy.copy(a) destructiveRemoveAll(a, value) return a a = [ 1, 2, 3, 4, 3, 2, 1 ] print("At first") print(" a =", a) destructiveRemoveAll(a, 2) print("After destructiveRemoveAll(a, 2)") print(" a =", a) b = nonDestructiveRemoveAll(a, 3) print("After b = nonDestructiveRemoveAll(a, 3)") print(" a =", a) print(" b =", b) c = alternateNonDestructiveRemoveAll(a, 1) print("After c = alternateNonDestructiveRemoveAll(a, 1)") print(" a =", a) print(" c =", c)

### Finding Elements

• Check for list membership: in
a = [ 2, 3, 5, 2, 6, 2, 2, 7 ] print("a =", a) print("2 in a =", (2 in a)) print("4 in a =", (4 in a))

• Check for list non-membership: not in
a = [ 2, 3, 5, 2, 6, 2, 2, 7 ] print("a =", a) print("2 not in a =", (2 not in a)) print("4 not in a =", (4 not in a))

• Count occurrences in list: list.count(item)
a = [ 2, 3, 5, 2, 6, 2, 2, 7 ] print("a =", a) print("a.count(1) =", a.count(1)) print("a.count(2) =", a.count(2)) print("a.count(3) =", a.count(3))

• Find index of item: list.index(item) and list.index(item, start)
• Example
a = [ 2, 3, 5, 2, 6, 2, 2, 7 ] print("a =", a) print("a.index(6) =", a.index(6)) print("a.index(2) =", a.index(2)) print("a.index(2,1) =", a.index(2,1)) print("a.index(2,4) =", a.index(2,4))

• Problem: crashes when item is not in list
a = [ 2, 3, 5, 2 ] print("a =", a) print("a.index(9) =", a.index(9)) # crashes! print("This line will not run!")

• Solution: use (item in list)
a = [ 2, 3, 5, 2 ] print("a =", a) if (9 in a): print("a.index(9) =", a.index(9)) else: print("9 not in", a) print("This line will run now!")

• Destructively (Modifying Lists)
• Add an item with list.append(item)
a = [ 2, 3 ] a.append(7) print(a)

• Add a list of items with list += list2 or list.extend(list2)
a = [ 2, 3 ] a += [ 11, 13 ] print(a) a.extend([ 17, 19 ]) print(a)

• Insert an item at a given index
a = [ 2, 3, 5, 7, 11 ] a.insert(2, 42) # at index 2, insert 42 print(a)

• Non-Destructively (Creating New Lists)
• Add an item with list1 + list2
a = [ 2, 3 ] b = a + [ 13, 17 ] print(a) print(b)

• Insert an item at a given index (with list slices)
a = [ 2, 3 ] b = a[:2] +  + a[2:] print(a) print(b)

• Destructive vs Non-Destructive Example
print("Destructive:") a = [ 2, 3 ] b = a a += [ 4 ] print(a) print(b) print("Non-Destructive:") a = [ 2, 3 ] b = a a = a + [ 4 ] # this overwrites a, but not the alias of b print(a) print(b)

### Removing Elements

• Destructively (Modifying Lists)
• Remove an item with list.remove(item)
a = [ 2, 3, 5, 3, 7, 6, 5, 11, 13 ] print("a =", a) a.remove(5) print("After a.remove(5), a=", a) a.remove(5) print("After another a.remove(5), a=", a)

• Remove an item at a given index with list.pop(index)
a = [ 2, 3, 4, 5, 6, 7, 8 ] print("a =", a) item = a.pop(3) print("After item = a.pop(3)") print(" item =", item) print(" a =", a) item = a.pop(3) print("After another item = a.pop(3)") print(" item =", item) print(" a =", a) # Remove last item with list.pop() item = a.pop() print("After item = a.pop()") print(" item =", item) print(" a =", a)

• Non-Destructively (Creating New Lists)
• Remove an item at a given index (with list slices)
a = [ 2, 3, 5, 3, 7, 5, 11, 13 ] print("a =", a) b = a[:2] + a[3:] print("After b = a[:2] + a[3:]") print(" a =", a) print(" b =", b)

### Looping Over Lists

• Looping with a normal for loop:
a = [ 2, 3, 5, 7 ] print("Here are the items in a with their indexes:") for index in range(len(a)): print("a[", index, "] =", a[index])

• Looping with a for each loop
# Lists and strings are both iterable types. # This means that we can iterate (loop) over them directly! a = [ 2, 3, 5, 7 ] print("Here are the items in a:") for item in a: print(item)

• Hazard: modifying inside a for loop
# IMPORTANT: don't change a list inside a for loop! The indexes will behave unpredictably. # This isn't a problem for strings because they aren't mutable. a = [ 2, 3, 5, 3, 7 ] print("a =", a) # Failed attempt to remove all the 3's for index in range(len(a)): if (a[index] == 3): # this eventually crashes! a.pop(index) print("This line will not run!")

• Also Hazard: modifying inside a for-each loop
# If we remove items in a for-each loop, the loop won't crash, # but it won't behave as we would expect either! a = [3, 3, 2, 3, 4] for item in a: # this won't reach every item in the list! if (item == 3): a.remove(item) print(a) # should be [2, 4], but there's still a 3 in there!

• Better: modifying inside a while loop
# Modify the list in a while loop instead of a for loop, # to control how indexes a = [ 2, 3, 5, 3, 7 ] print("a =", a) # Successful attempt to remove all the 3's index = 0 while (index < len(a)): if (a[index] == 3): a.pop(index) else: index += 1 print("This line will run!") print("And now a =", a)

### List Methods: Sorting and Reversing

Lists have many built-in methods. It's common for these methods to be implemented both destructively and non-destructively.

• Destructively with list.sort() or list.reverse()
a = [ 7, 2, 5, 3, 5, 11, 7 ] print("At first, a =", a) a.sort() print("After a.sort(), a =",a) a = [ 2, 3, 5, 7 ] print("Here are the items in reverse:") a.reverse() for item in a: print(item) print(a)

• Non-Destructively with sorted(list) and reversed(list)
a = [ 7, 2, 5, 3, 5, 11, 7 ] print("At first") print(" a =", a) b = sorted(a) print("After b = sorted(a)") print(" a =", a) print(" b =", b) a = [ 2, 3, 5, 7 ] print("Here are the items in reverse:") for item in reversed(a): print(item) print(a)

• More list methods
For most list methods, see this table and this list of list methods.

### Summary: Destructive and Non-destructive

Some destructive vs. non-destructive list analogs, for:

a = ['cat','dog','pig','cow']

Destructive (aliasing) Nondestructive
b = a
b = copy.copy(a)
b = a[:]
b = a + []
b = list(a)
a.append('axolotl') # just the elem
a.extend(['axolotl']) # watch the brackets
a += ['axolotl'] # also needs brackets
a = a + ['axolotl']
x = a.pop() # equivalent to x = a.pop(-1)
x = a[-1] # Get the last element a = a[:-1] # Then make the smaller copy
x = a.pop(2) # index
x = a a = a[:2] + a[3:]
a.remove('pig')
i = a.index('pig') a = a[:i] + a[i+1:]
a.insert(2, 'axolotl') # index, element
a = a[:2] + ['axolotl'] + a[2:]
a.sort()
a = sorted(a)
a.reverse()
a = list(reversed(a))
(Need to wrap with list() because the reversed function evaluates to something called an iterator.)

Tips:

• List slicing is always nondestructive and "adding" lists is (usually) nondestructive
• a.something() calls are destructive (usually)
• Know the type of arguments required by the techinques above, e.g., element value or index; with or without brackets

### Tuples (Immutable Lists)

Tuples are exactly like lists, except they are immutable. We cannot change the values of a tuple.

• Tuple syntax
t = (1, 2, 3) print(type(t), len(t), t) a = [1, 2, 3] t = tuple(a) print(type(t), len(t), t)

• Tuples are immutable
t = (1, 2, 3) print(t) t = 42 # crash! print(t)

• Parallel (tuple) assignment
(x, y) = (1, 2) print(x) print(y) # tuples are useful for swapping! (x, y) = (y, x) print(x) print(y)

• Singleton tuple syntax
t = (42) print(type(t), t*5) t = (42,) # use a comma to force the type print(type(t), t*5)

### List Comprehensions

List comprehensions are a handy way to create lists using simple loops all in one line.
# Long way a = [] for i in range(10): a.append(i) print(a) # Short way a = [i for i in range(10)] print(a) # We can also add conditionals at the end (but keep it simple!) a = [(i*100) for i in range(20) if i%5 == 0] print(a)

### Converting Between Lists and Strings

# use list(s) to convert a string to a list of characters a = list("wahoo!") print(a) # prints: ['w', 'a', 'h', 'o', 'o', '!'] # use s1.split(s2) to convert a string to a list of strings delimited by s2 a = "How are you doing today?".split(" ") print(a) # prints ['How', 'are', 'you', 'doing', 'today?'] # use "".join(a) to convert a list of characters to a single string print("".join(a)) # prints: Howareyoudoingtoday? # "".join(a) also works on a list of strings (not just single characters) a = ["parsley", "is", "gharsley"] # by Ogden Nash! print("".join(a)) # prints: parsleyisgharsley print(" ".join(a)) # prints: parsley is gharsley

### Worked Examples Using Lists

If you want to review more examples of problem-solving with lists, you can find several worked examples here. We'll go over some of these in class as well.