2.6. Lecture 5: Python 3¶
Before this class you should:
Read Think Python:
Chapter 8: Strings;
Chapter 10: Lists;
Chapter 11: Dictionaries; and
Chapter 12: Tuples
Before next class you should:
Read Think Python:
Chapter 15: Classes and objects;
Chapter 16: Classes and functions;
Chapter 17: Classes and methods; and
Chapter 18: Inheritance
Note taker: Edison Liang
2.6.1. Overview¶
Today’s class focused on core Python data structures and programming patterns that will be used throughout the rest of the course.
During the lecture, we:
Reviewed course logistics, including upcoming homework deadlines, the pacing of assignments, and the format and expectations of the first lab test
Revisited integer division, the modulus operator, Boolean expressions, and user input, with an emphasis on type conversion and common runtime errors
Introduced strings as ordered, immutable sequences of characters and explored indexing, slicing, traversal, and length
Discussed key string patterns such as searching, counting characters, membership testing, comparisons, and commonly used string methods
Introduced lists and compared their mutability to strings, then practiced traversing, modifying, slicing, and applying common list operations and methods
Learned the mapping, filtering, and reducing patterns and how they apply to processing both lists and strings
Combined strings and lists using split and join methods and discussed how these conversions are useful for text processing
Discussed object aliasing and mutability, including how shared references can lead to unintended side effects
Introduced dictionaries as key-value data structures and practiced using them for fast lookup, counting, and histogram construction
2.6.2. Strings¶
Strings are ordered sequences of characters used to represent text. They are immutable, meaning their contents cannot be changed once created and any operation that modifies a string will actually create a new one. A string is created using quotes and supports indexing, slicing, and many built-in methods for text processing.
String Indexing
Each character in a string has a position starting at index 0. Negative indices count backward from the end of the string.
The following code stores the string "banana" in the variable word. The expression word[0] accesses the first character in the string, since indexing starts at 0. The expression word[-1] accesses the last character by counting backward from the end of the string.
word = "banana"
word[0] # 'b'
word[-1] # 'a'
String Length
The len() function returns the number of characters in a string. The last valid index is always one less than the length.
The following code calls the built-in len() function on the string stored in word. It counts how many characters are in the string, including repeated letters. The result is 6, which means valid indices range from 0 to 5.
len(word) # 6
String Slicing
Slicing extracts a substring using a start and end index. The start index is inclusive, the end index is exclusive, and omitted indices default to the beginning or end.
Example:
word[1:4] # 'ana'
word[:3] # 'ban'
word[3:] # 'ana'
In the code above, the first slice extracts characters from index 1 up to but not including index 4. The second slice omits the start index, so Python assumes slicing from the beginning of the string. The third slice omits the end index, so Python slices from index 3 to the end of the string.
String Immutability
Strings cannot be modified directly using assignment. To change a string, you must construct a new one using concatenation or slicing.
The code below creates a new string rather than modifying the original one. It takes the substring of word starting at index 1 and concatenates it with the letter "j". The original string word remains unchanged, demonstrating string immutability.
word = "hello"
new_word = "j" + word[1:] # 'jello'
Searching in Strings
The find() method searches for a character or substring and returns its first index. If the value is not found, it returns -1.
Example:
word.find("a") # 1
word.find("x") # -1
In the code above, the first line searches for the character "a" in the string and returns the index of its first occurrence. Since "a" appears at index 1, the result is 1. The second line searches for "x", which does not appear in the string, so the method returns -1.
String Methods
String methods perform common operations such as changing case or checking character properties. These methods return new values and do not modify the original string.
In the code below, the upper() method returns a new string where all letters are converted to uppercase. The islower() method checks whether all letters in the string are lowercase and returns a boolean value. In both cases, the original string stored in word remains unchanged.
word.upper() # 'BANANA'
word.islower() # True
Membership Operator
The in operator checks whether a character or substring exists in a string. It returns True or False.
Example:
"a" in "banana" # True
The code above checks whether the character "a" appears anywhere in the string "banana". Since "a" occurs multiple times, the expression evaluates to True. This operator is commonly used in conditional statements.
String Comparison
Strings are compared character by character using ASCII values. Uppercase letters come before lowercase letters, so converting strings to the same case avoids unexpected results.
Example:
"Apple".lower() < "banana"
The code above converts "Apple" to lowercase before comparing it to "banana". Both strings are now lowercase, making the comparison more intuitive. The comparison checks lexicographical (alphabetical) order character by character.
2.6.3. Traversal with a for loop¶
Traversal refers to accessing each element of a sequence one at a time, most commonly using a for loop. It allows programs to inspect, process, or transform every element in a string or list. The for loop is preferred over a while loop because it is simpler and less error prone.
Traversing a String
Example:
for char in "banana":
print(char)
The loop above iterates through each character in the string "banana" one at a time. On each iteration, the variable char stores the current character. The print statement outputs each character on its own line.
Counting Pattern
The counting pattern is used to track how many times something occurs. This is a very common pattern that could appear on assessments.
Example:
count = 0
for char in "banana":
if char == "a":
count += 1
In the code above, the variable count is initialized to zero before the loop starts. Each time the loop encounters the character "a", the condition evaluates to True and count is increased by one. After the loop finishes, count stores the total number of occurrences of "a".
Accumulation (Reduction) Pattern
Accumulation combines values from a sequence into a single result, such as a sum. Python provides built-in functions like sum() for common cases.
Example:
numbers = [1, 2, 3, 4]
total = sum(numbers)
In the code above, the list numbers stores a sequence of integers. The sum() function adds all elements in the list together and returns the result. This single value is then stored in the variable total.
2.6.4. Lists¶
Lists are ordered, mutable collections that can store values of any data type. Since they are mutable, their elements can be changed, added, or removed after creation. Lists are ideal for storing data that needs to be updated or processed repeatedly.
Creating and Modifying Lists
In the code below, the first line creates a list containing three integers. The second line modifies the element at index 1 by assigning it a new value. This change directly updates the list, demonstrating list mutability.
numbers = [10, 20, 30]
numbers[1] = 99
numbers # [10, 99, 30]
Traversing Lists
Lists can be traversed directly by element or by index. Using indices is useful when you need to modify list elements.
Example:
for n in numbers:
print(n)
for i in range(len(numbers)):
numbers[i] *= 2
In the code above, the first loop prints each element in the list without modifying it. The second loop uses indices to access and update each element in the list. Each value is multiplied by 2 and stored back into the list at the same index.
List Operations
Lists support concatenation and repetition. These operations create new lists rather than modifying the originals.
Example:
[1, 2] + [3, 4] # [1, 2, 3, 4]
[1, 2] * 3 # [1, 2, 1, 2, 1, 2]
In the code above, the first expression combines two lists into a single new list. The second expression repeats the list [1, 2] three times in sequence. In both cases, the original lists remain unchanged.
List Slicing
List slicing works like string slicing but allows assignment. This makes it useful for replacing multiple elements at once.
Example:
nums = [1, 2, 3, 4] # [1, 2, 3, 4]
nums[1:3] = [7, 8] # [1, 7, 8, 4]
The slice nums[1:3] refers to the elements at indices 1 and 2. Assigning a new list replaces those elements with new values. The list length stays the same in this example, but slicing can also change list size.
List Methods
List methods modify the list in place and usually return None. Common methods include adding, removing, and sorting elements.
Example:
nums.append(5)
nums.sort()
nums.remove(7)
In the code above, the append() method adds a new element to the end of the list. The sort() method rearranges the list elements in ascending order. The remove() method deletes the first occurrence of the specified value from the list.
Aliasing and Mutability
When two variables refer to the same list, changes made through one variable affect the other. This behavior is called aliasing and can cause unintended side effects.
Example:
a = [1, 2]
b = a
b[0] = 99
The variable b is assigned the same list object as a, not a copy. When b[0] is modified, the change is visible through a as well. This demonstrates how aliasing interacts with list mutability.
2.6.5. Dictionaries¶
Dictionaries store data as key-value pairs, where each key maps to a specific value. They allow for very fast data lookup because values are accessed using keys rather than numeric indices.
They are commonly used for counting occurrences, mapping relationships, and organizing structured data. Each key in a dictionary must be unique, and keys are typically strings or numbers, while values can be almost any data type.
Mappings Between Keys and Values
A dictionary represents a direct mapping between keys and values, meaning each key is associated with exactly one value. This makes dictionaries ideal for representing relationships like names to scores, words to counts, or IDs to records. Accessing a value by its key is both clear and efficient.
Features of Dictionaries
Keys are immutable and hashed
Dictionary keys must be immutable types such as strings, numbers, or tuples. Python uses a hash of the key to store and retrieve values efficiently, which is why mutable types like lists cannot be used as keys.
Values can be any type
Dictionary values can be strings, numbers, lists, other dictionaries, or even objects. This flexibility allows dictionaries to store complex and structured data without restriction.
Examples:
eng2sp = dict() # creates an empty dictionary
The code above initializes an empty dictionary using the dict() constructor. At this point, the dictionary contains no key-value pairs. This is useful when you want to build a dictionary incrementally.
eng2sp['one'] = 'uno' # adds a key-value pair
The code above inserts a new entry into the dictionary where the key 'one' maps to the value 'uno'. If the key already existed, its value would be updated instead. Dictionary assignments always operate on keys, not positions.
eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'} # creates a dictionary with items
The code above creates a dictionary with multiple key-value pairs defined at once. This syntax is often clearer and more concise when the contents are known ahead of time. Each key is unique and maps to its corresponding value.
Nested Dictionaries
Dictionaries can store other dictionaries as values, creating nested dictionaries. This is useful for representing hierarchical or structured data such as student records, configuration settings, or JSON-like data formats. Accessing nested values requires chaining keys together.
Creating and Accessing Dictionaries
In the code below, the first line creates a dictionary where names are keys and scores are values. The second line accesses the value associated with the key "Alice". Dictionary lookup is fast because Python uses a hash table, allowing average-case lookup time of O(1). In contrast, searching for a value in a list requires scanning elements sequentially, which takes O(n) time in the worst case.
grades = {"Alice": 90, "Bob": 85}
grades["Alice"]
Dictionary Operations
You can check for the existence of a key, determine the size of a dictionary, and retrieve collections of keys or values.
Example:
len(grades)
"Alice" in grades
The len() function returns the number of key-value pairs in the dictionary. The in operator checks whether a specific key exists. This is commonly used to avoid errors when accessing dictionary values.
Counting with Dictionaries (Histogram)
Dictionaries are ideal for counting occurrences because each key stores its own count. The get() method simplifies updating values safely.
Example:
counts = {}
for char in "banana":
counts[char] = counts.get(char, 0) + 1
In the code above, the dictionary counts starts empty before the loop. For each character, get() retrieves the current count or returns 0 if the key does not exist. The count is then incremented and stored back into the dictionary.
Traversing Dictionaries
Dictionaries are traversed by looping over their keys. Sorting keys before traversal produces consistent output.
Example:
for key in sorted(counts):
print(key, counts[key])
In the code above, the sorted() function returns a sorted list of dictionary keys. The loop iterates through these keys in order, ensuring predictable output. Each key is printed alongside its corresponding value.