No history yet

Advanced Data Structures

Beyond Lists and Tuples

You're already familiar with storing data in sequence using lists and tuples. These are great when the order of items matters. But what if you need to store data based on relationships, not just order? This is where dictionaries come in. Dictionaries store data as key-value pairs, which lets you retrieve a value by knowing its unique key, much like looking up a word in a real dictionary.

Think of a list as a numbered locker system where you access items by their position (0, 1, 2...). A dictionary is a named locker system where you use a specific name (the key) to get what's inside (the value).

# Creating a dictionary of user data
user = {
    'username': 'code_master',
    'id': 101,
    'is_active': True
}

# Accessing a value by its key
print(user['username']) # Output: code_master

Smarter Dictionary Use

Dictionaries can get much more complex. The value associated with a key can be any data type, including another dictionary. This allows for creating nested structures that can represent complex, hierarchical data. For example, you could store multiple attributes for a user within a larger dictionary of users.

# A nested dictionary
users = {
    'user_101': {
        'name': 'Alice',
        'email': 'alice@example.com'
    },
    'user_102': {
        'name': 'Bob',
        'email': 'bob@example.com'
    }
}

# Accessing nested data
print(users['user_101']['name']) # Output: Alice

A common issue arises when you try to access a key that doesn't exist, which causes a KeyError. To avoid this, you can use the .get() method. It safely retrieves a key and allows you to provide a default value if the key is not found, preventing your program from crashing.

user = {'username': 'code_master', 'id': 101}

# Safe access with .get()
status = user.get('status', 'inactive')
print(status) # Output: inactive

# Unsafe access that would raise an error
# print(user['status']) # This would cause a KeyError

The Power of Sets

While dictionaries are for mapping keys to values, sets are for managing unique collections of items. A set is an unordered collection with no duplicate elements. They are highly optimized for checking whether an item is present in the collection, a process known as membership testing.

Imagine you have a long list of customer IDs and you want to find all the unique customers. A set is the perfect tool for this. You can also perform mathematical set operations, such as finding commonalities (intersection) or combining unique items (union).

# A list with duplicate numbers
numbers = [1, 2, 2, 3, 4, 4, 4, 5]

# Creating a set automatically removes duplicates
unique_numbers = set(numbers)
print(unique_numbers) # Output: {1, 2, 3, 4, 5}

# Fast membership testing
print(3 in unique_numbers) # Output: True
print(10 in unique_numbers) # Output: False

Set theory provides a formal way to describe these operations. The union of two sets contains all unique elements from both. The intersection contains only the elements that appear in both sets. The difference contains elements from the first set that are not in the second.

set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}

# Union: all unique elements from both sets
print(set_a | set_b)  # Output: {1, 2, 3, 4, 5, 6}

# Intersection: elements present in both sets
print(set_a & set_b)  # Output: {3, 4}

# Difference: elements in set_a but not in set_b
print(set_a - set_b)  # Output: {1, 2}

Because of hashing, only immutable (unchangeable) data types can be used as dictionary keys or set elements. This includes strings, numbers, and tuples. Lists and other dictionaries cannot be keys because their contents can change, which would change their hash value.

Specialized Collections

Python's collections module provides even more powerful data structures. Two of the most useful are defaultdict and Counter.

A defaultdict is like a regular dictionary, but if you try to access a key that doesn't exist, it automatically creates an entry for you with a default value, instead of raising a KeyError. This is incredibly handy for grouping or counting items.

from collections import defaultdict

# Create a defaultdict where the default value is an integer (0)
dd = defaultdict(int)

words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']

for word in words:
    dd[word] += 1

# dd is now defaultdict(<class 'int'>, {'apple': 3, 'banana': 2, 'orange': 1})
print(dd['apple']) # Output: 3
print(dd['grape']) # Output: 0 (and 'grape' is now a key in dd)

The Counter is a specialized dictionary designed specifically for counting hashable objects. It simplifies the counting pattern we just saw with defaultdict.

from collections import Counter

words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']

# The Counter automatically counts the items in the list
word_counts = Counter(words)

# word_counts is Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(word_counts['apple']) # Output: 3

# It also has helpful methods, like finding the most common items
print(word_counts.most_common(1)) # Output: [('apple', 3)]

Time to test your understanding of these advanced data structures.

Quiz Questions 1/6

What is the primary characteristic of a Python dictionary?

Quiz Questions 2/6

Consider the following code snippet:

user_data = {
    'id': 101,
    'info': {
        'name': 'Alice',
        'email': 'alice@example.com'
    }
}

How would you access the value 'alice@example.com'?

By mastering dictionaries and sets, you can write more efficient and readable Python code, especially when dealing with large and complex datasets.