Bài 3.16. Sử dụng dict trong Python hiệu quả

Chào mừng các bạn quay trở lại với series học Python! Trong bài học này, chúng ta sẽ đào sâu vào một trong những cấu trúc dữ liệu quan trọng và mạnh mẽ nhất của Python: dict (dictionary). Không chỉ dừng lại ở việc biết cách tạo và truy cập phần tử, chúng ta sẽ cùng nhau khám phá những kỹ thuật để sử dụng dict một cách thực sự hiệu quả, giúp code của bạn không chỉ chạy nhanh hơn mà còn dễ đọc và bảo trì hơn.

Ôn lại những điều cơ bản về Dictionary

Trước khi đi vào các kỹ thuật nâng cao, hãy cùng nhắc lại nhanh những kiến thức nền tảng về dict:

  • Khái niệm: dict là một tập hợp các cặp key-value (khóa-giá trị). Mỗi key trong dictionary là duy nhất và được sử dụng để truy cập value tương ứng.
  • Tính chất:

    • Mutable: Bạn có thể thay đổi nội dung của dictionary sau khi tạo (thêm, sửa, xóa phần tử).
    • Unordered (trước Python 3.7) / Ordered (từ Python 3.7+): Từ Python 3.7, dictionary ghi nhớ thứ tự các phần tử được thêm vào. Tuy nhiên, bạn vẫn nên truy cập phần tử qua key thay vì dựa vào thứ tự.
    • Key phải là các đối tượng immutable (không thể thay đổi), ví dụ: số, chuỗi, tuple. Value có thể là bất kỳ kiểu dữ liệu nào.
  • Cách tạo:

    # Sử dụng dấu ngoặc nhọn {}
    student = {
        "name": "Alice",
        "age": 20,
        "major": "Computer Science"
    }
    print(student)
    
    # Sử dụng hàm dict()
    config = dict(host="localhost", port=8080, debug=True)
    print(config)
    

    Giải thích: Hai cách trên đều tạo ra dictionary. Cách dùng {} phổ biến hơn cho việc khởi tạo trực tiếp. dict() hữu ích khi tạo từ các cặp key-value hoặc chuyển đổi từ cấu trúc khác.

  • Truy cập phần tử:

    student = {"name": "Alice", "age": 20}
    print(student["name"])  # Output: Alice
    
    # Nếu key không tồn tại, sẽ gây lỗi KeyError
    # print(student["grade"]) # Uncommenting this line will raise KeyError
    

    Giải thích: Sử dụng dấu ngoặc vuông []key để lấy value tương ứng. Cần lưu ý rằng nếu key không tồn tại, chương trình sẽ dừng lại với lỗi KeyError. Chúng ta sẽ xem cách xử lý việc này hiệu quả hơn ở phần sau.

  • Thêm/Cập nhật phần tử:

    student = {"name": "Alice", "age": 20}
    
    # Cập nhật giá trị của key 'age'
    student["age"] = 21
    print(student) # Output: {'name': 'Alice', 'age': 21}
    
    # Thêm cặp key-value mới
    student["grade"] = "A"
    print(student) # Output: {'name': 'Alice', 'age': 21, 'grade': 'A'}
    

    Giải thích: Nếu key đã tồn tại, việc gán giá trị mới sẽ cập nhật value. Nếu key chưa tồn tại, một cặp key-value mới sẽ được thêm vào dictionary.

  • Xóa phần tử:

    student = {'name': 'Alice', 'age': 21, 'grade': 'A'}
    
    # Sử dụng del
    del student["grade"]
    print(student) # Output: {'name': 'Alice', 'age': 21}
    
    # Sử dụng pop() - trả về giá trị đã xóa
    removed_age = student.pop("age")
    print(f"Removed age: {removed_age}") # Output: Removed age: 21
    print(student) # Output: {'name': 'Alice'}
    

    Giải thích: del xóa cặp key-value trực tiếp. pop(key) xóa cặp key-value và trả về value tương ứng. Nếu key không tồn tại, cả hai đều gây KeyError (trừ khi pop() được cung cấp giá trị mặc định).

Tại sao Dictionary lại hiệu quả?

Điểm mạnh cốt lõi của dict nằm ở tốc độ truy cập dữ liệu.

  1. Truy cập O(1) (Trung bình): Dictionary trong Python được triển khai bằng bảng băm (hash table). Điều này có nghĩa là, trung bình, thời gian để tìm một value dựa trên key của nó là không đổi, bất kể dictionary có bao nhiêu phần tử. Ký hiệu là O(1). So sánh với List: Để tìm một phần tử trong list (mà không biết vị trí index), bạn thường phải duyệt qua từng phần tử một cho đến khi tìm thấy. Trung bình, việc này mất thời gian tỷ lệ thuận với số lượng phần tử trong list (O(n)). Khi dữ liệu lớn, sự khác biệt về tốc độ là rất đáng kể.

    Ví dụ (minh họa ý tưởng, không phải benchmark chính xác):

    import time
    
    # Tạo dữ liệu lớn
    n = 1000000
    my_list = list(range(n))
    my_dict = {i: i for i in range(n)}
    search_key = n - 1 # Key cần tìm ở cuối
    
    # Đo thời gian tìm kiếm trong list
    start_time = time.time()
    if search_key in my_list: # Toán tử 'in' trên list là O(n)
        pass
    end_time = time.time()
    print(f"List search time: {end_time - start_time:.6f} seconds")
    
    # Đo thời gian tìm kiếm trong dict
    start_time = time.time()
    if search_key in my_dict: # Toán tử 'in' trên dict là O(1) trung bình
        pass
    end_time = time.time()
    print(f"Dict search time: {end_time - start_time:.6f} seconds")
    
    # Lưu ý: Kết quả có thể thay đổi tùy máy và phiên bản Python,
    # nhưng dict thường nhanh hơn đáng kể cho tìm kiếm key.
    

    Giải thích: Đoạn code trên so sánh thời gian tìm kiếm một phần tử trong list lớn và dict lớn. Bạn sẽ thấy thời gian tìm kiếm trong dict (kiểm tra sự tồn tại của key) thường nhanh hơn rất nhiều so với tìm kiếm trong list.

  2. Tính dễ đọc và cấu trúc: Sử dụng key có ý nghĩa (ví dụ: "name", "age") làm cho code trở nên dễ hiểu hơn so với việc sử dụng index số học (ví dụ: data[0], data[1]). Dictionary giúp biểu diễn dữ liệu có cấu trúc một cách tự nhiên.

Các kỹ thuật sử dụng Dictionary hiệu quả

Biết cách sử dụng các phương thức và kỹ thuật phù hợp sẽ giúp bạn khai thác tối đa sức mạnh của dict.

1. Kiểm tra sự tồn tại của Key một cách an toàn

Thay vì truy cập trực tiếp bằng [] và có nguy cơ gặp KeyError, hãy sử dụng các cách sau:

  • Toán tử in: Cách Pythonic nhất để kiểm tra xem key có tồn tại hay không.

    student = {"name": "Bob", "major": "Physics"}
    
    if "age" in student:
        print(f"Age: {student['age']}")
    else:
        print("Age key not found.") # Output: Age key not found.
    
    if "name" in student:
        print(f"Name: {student['name']}") # Output: Name: Bob
    

    Giải thích: key in dictionary trả về True nếu key tồn tại, False nếu không. Đây là cách kiểm tra nhanh và rõ ràng.

  • Phương thức get(key, default=None): Lấy giá trị của key. Nếu key không tồn tại, trả về giá trị default (mặc định là None) thay vì gây lỗi.

    student = {"name": "Charlie"}
    
    # Lấy tuổi, nếu không có thì trả về None
    age = student.get("age")
    print(f"Age: {age}") # Output: Age: None
    
    # Lấy tuổi, nếu không có thì trả về giá trị mặc định -1
    age_default = student.get("age", -1)
    print(f"Age with default: {age_default}") # Output: Age with default: -1
    
    # Lấy tên (key tồn tại)
    name = student.get("name", "Unknown")
    print(f"Name: {name}") # Output: Name: Charlie
    

    Giải thích: get() rất hữu ích khi bạn muốn lấy một giá trị nhưng không chắc chắn key có tồn tại hay không, và muốn cung cấp một giá trị dự phòng.

  • Phương thức setdefault(key, default=None): Hoạt động tương tự get(), nhưng có thêm một bước: Nếu key không tồn tại, nó sẽ thêm cặp key: default vào dictionary trước khi trả về default. Nếu key đã tồn tại, nó chỉ trả về giá trị hiện tại.

    stats = {"hits": 100}
    
    # Key 'misses' chưa tồn tại, thêm 'misses': 0 vào dict và trả về 0
    misses = stats.setdefault("misses", 0)
    print(f"Misses: {misses}") # Output: Misses: 0
    print(stats) # Output: {'hits': 100, 'misses': 0}
    
    # Key 'hits' đã tồn tại, chỉ trả về giá trị hiện tại (100)
    hits = stats.setdefault("hits", 50) # Giá trị mặc định 50 bị bỏ qua
    print(f"Hits: {hits}") # Output: Hits: 100
    print(stats) # Output: {'hits': 100, 'misses': 0} (không thay đổi)
    

    Giải thích: setdefault() cực kỳ tiện lợi khi bạn muốn đảm bảo một key tồn tại với một giá trị khởi tạo nếu nó chưa có, ví dụ như khi đếm hoặc xây dựng dictionary chứa list.

2. Duyệt qua Dictionary

Có nhiều cách để lặp qua các phần tử của dict:

  • Duyệt qua Keys (Mặc định):

    config = {"host": "localhost", "port": 8080, "debug": True}
    print("Keys:")
    for k in config: # Mặc định duyệt qua keys
        print(k)
    # Hoặc rõ ràng hơn:
    # for k in config.keys():
    #     print(k)
    

    Giải thích: Vòng lặp for trực tiếp trên dictionary sẽ duyệt qua các key của nó. config.keys() trả về một đối tượng view chứa các keys.

  • Duyệt qua Values:

    config = {"host": "localhost", "port": 8080, "debug": True}
    print("\nValues:")
    for v in config.values():
        print(v)
    

    Giải thích: config.values() trả về một đối tượng view chứa các value.

  • Duyệt qua Key-Value Pairs (Phổ biến nhất):

    config = {"host": "localhost", "port": 8080, "debug": True}
    print("\nItems (Key-Value pairs):")
    for key, value in config.items():
        print(f"  {key}: {value}")
    

    Giải thích: config.items() trả về một đối tượng view chứa các cặp (key, value) dạng tuple. Đây thường là cách duyệt hữu ích nhất vì bạn có cả keyvalue trong mỗi lần lặp.

3. Dictionary Comprehensions

Tương tự như list comprehensions, dictionary comprehensions cung cấp một cách ngắn gọn và hiệu quả để tạo dict từ các iterable khác.

# Tạo dict bình phương các số từ 0 đến 4
squares = {x: x*x for x in range(5)}
print(squares) # Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Tạo dict từ list, lọc các số chẵn
numbers = [1, 2, 3, 4, 5, 6]
even_squares = {n: n*n for n in numbers if n % 2 == 0}
print(even_squares) # Output: {2: 4, 4: 16, 6: 36}

# Đảo ngược key và value của một dict khác (cẩn thận nếu value không unique)
original = {'a': 1, 'b': 2, 'c': 3}
swapped = {v: k for k, v in original.items()}
print(swapped) # Output: {1: 'a', 2: 'b', 3: 'c'}

Giải thích: Cú pháp {key_expression: value_expression for item in iterable [if condition]} cho phép tạo dictionary một cách nhanh chóng và dễ đọc hơn so với việc dùng vòng lặp for và gán giá trị từng bước.

4. Gộp Dictionary

Có hai cách phổ biến để gộp nội dung từ một dictionary vào dictionary khác:

  • Phương thức update(): Thay đổi (mutate) dictionary gốc bằng cách thêm các cặp key-value từ dictionary khác. Nếu key trùng lặp, giá trị từ dictionary được truyền vào sẽ ghi đè lên giá trị cũ.

    dict1 = {'a': 1, 'b': 2}
    dict2 = {'b': 3, 'c': 4}
    
    dict1.update(dict2)
    print(dict1) # Output: {'a': 1, 'b': 3, 'c': 4}
    # Lưu ý: dict1 đã bị thay đổi
    

    Giải thích: update() sửa đổi trực tiếp dict1. Key 'b' chung được cập nhật giá trị từ dict2.

  • Toán tử | (Merge - Python 3.9+): Tạo ra một dictionary mới chứa các phần tử từ cả hai dictionary. Nếu có key trùng lặp, giá trị từ dictionary bên phải sẽ được ưu tiên.

    dict1 = {'a': 1, 'b': 2}
    dict2 = {'b': 3, 'c': 4}
    
    merged_dict = dict1 | dict2
    print(merged_dict) # Output: {'a': 1, 'b': 3, 'c': 4}
    print(dict1)       # Output: {'a': 1, 'b': 2} (không đổi)
    print(dict2)       # Output: {'b': 3, 'c': 4} (không đổi)
    
    # Cũng có thể dùng toán tử |= để update tại chỗ (tương tự update())
    # dict1 |= dict2
    # print(dict1) # Output: {'a': 1, 'b': 3, 'c': 4}
    

    Giải thích: Toán tử | là cách hiện đại và thường được ưa thích hơn (khi dùng Python 3.9+) để gộp dict vì nó không làm thay đổi các dict gốc, giúp tránh các tác dụng phụ không mong muốn.

5. Sử dụng collections.defaultdict

Khi bạn cần một dictionary mà các value là các tập hợp (như list, set, hoặc counter) và bạn thường xuyên cần thêm phần tử vào các tập hợp đó, việc kiểm tra key tồn tại và khởi tạo tập hợp nếu cần có thể trở nên lặp đi lặp lại. defaultdict từ module collections giải quyết vấn đề này rất thanh lịch.

defaultdict hoạt động giống như dict thông thường, ngoại trừ việc nó nhận một factory function (hàm tạo) khi khởi tạo. Nếu bạn cố gắng truy cập một key không tồn tại, defaultdict sẽ tự động gọi factory function này để tạo ra giá trị mặc định cho key đó, thêm nó vào dictionary, và trả về giá trị mới tạo.

from collections import defaultdict

# Ví dụ: Nhóm các từ theo chữ cái đầu tiên
words = ["apple", "ant", "banana", "bat", "cat", "car"]

# Sử dụng defaultdict với list làm factory function
words_by_letter = defaultdict(list)

for word in words:
    first_letter = word[0]
    words_by_letter[first_letter].append(word) # Không cần kiểm tra key tồn tại!

print(words_by_letter)
# Output: defaultdict(<class 'list'>, {'a': ['apple', 'ant'], 'b': ['banana', 'bat'], 'c': ['cat', 'car']})

# Truy cập key chưa có, nó sẽ tự tạo list rỗng
print(words_by_letter['z']) # Output: []
print(words_by_letter) # Output bây giờ có thêm key 'z': defaultdict(<class 'list'>, {..., 'z': []})

# So sánh với cách dùng dict thường + setdefault
words_by_letter_dict = {}
for word in words:
    first_letter = word[0]
    # Cách 1: Dùng setdefault
    words_by_letter_dict.setdefault(first_letter, []).append(word)
    # Cách 2: Dùng if
    # if first_letter not in words_by_letter_dict:
    #     words_by_letter_dict[first_letter] = []
    # words_by_letter_dict[first_letter].append(word)

print(words_by_letter_dict)
# Output: {'a': ['apple', 'ant'], 'b': ['banana', 'bat'], 'c': ['cat', 'car']}

Giải thích: Với defaultdict(list), khi truy cập words_by_letter[first_letter] lần đầu tiên cho một chữ cái, nếu key đó chưa tồn tại, nó sẽ tự động gọi list() để tạo một list rỗng, gán nó cho key đó, rồi thực hiện phép append(). Điều này làm code ngắn gọn và rõ ràng hơn nhiều so với việc phải tự kiểm tra và khởi tạo bằng dict thường. Các factory phổ biến khác là int (mặc định là 0, hữu ích cho việc đếm), set, v.v.

Các trường hợp sử dụng phổ biến và hiệu quả

Dictionary cực kỳ linh hoạt và xuất hiện trong vô số tình huống lập trình:

  1. Đếm tần suất xuất hiện: Rất hiệu quả để đếm số lần xuất hiện của các phần tử trong một chuỗi hoặc list.

    from collections import Counter, defaultdict
    
    text = "this is a sample text with sample words"
    words = text.split()
    
    # Cách 1: Dùng dict thường và get()
    word_counts_get = {}
    for word in words:
        word_counts_get[word] = word_counts_get.get(word, 0) + 1
    print(f"Counts (get): {word_counts_get}")
    
    # Cách 2: Dùng defaultdict(int)
    word_counts_defaultdict = defaultdict(int)
    for word in words:
        word_counts_defaultdict[word] += 1 # Tự động khởi tạo là 0 nếu chưa có
    print(f"Counts (defaultdict): {dict(word_counts_defaultdict)}") # Chuyển về dict thường để in đẹp hơn
    
    # Cách 3: Dùng collections.Counter (Cách tối ưu nhất cho việc đếm)
    word_counts_counter = Counter(words)
    print(f"Counts (Counter): {dict(word_counts_counter)}")
    

    Giải thích: Cả ba cách đều đạt được mục tiêu. get() là cách cơ bản. defaultdict(int) thanh lịch hơn. Counter là lớp được thiết kế đặc biệt cho việc đếm, cung cấp thêm các phương thức hữu ích (như most_common()).

  2. Nhóm dữ liệu: Gom các đối tượng có cùng thuộc tính lại với nhau.

    students = [
        {"name": "Alice", "grade": "A"},
        {"name": "Bob", "grade": "B"},
        {"name": "Charlie", "grade": "A"},
        {"name": "David", "grade": "C"},
        {"name": "Eve", "grade": "B"},
    ]
    
    students_by_grade = defaultdict(list)
    for student in students:
        students_by_grade[student["grade"]].append(student["name"])
    
    print(dict(students_by_grade))
    # Output: {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Eve'], 'C': ['David']}
    

    Giải thích: Sử dụng defaultdict(list) giúp nhóm sinh viên theo điểm số một cách dễ dàng. Key là điểm số, value là list tên các sinh viên đạt điểm đó.

  3. Lưu trữ cấu hình: Dictionary là cách tự nhiên để lưu trữ các cài đặt hoặc cấu hình.

    db_config = {
        "host": "127.0.0.1",
        "port": 5432,
        "username": "admin",
        "password": "secure_password",
        "database": "production_db"
    }
    
    print(f"Connecting to database '{db_config['database']}' on host {db_config.get('host', 'localhost')}")
    

    Giải thích: Dùng key dạng chuỗi giúp dễ dàng truy cập các giá trị cấu hình theo tên. get() được dùng để cung cấp giá trị mặc định an toàn.

  4. Thay thế câu lệnh switch/case (Dispatching): Python không có cấu trúc switch/case như các ngôn ngữ khác, nhưng dictionary có thể mô phỏng hành vi này một cách hiệu quả, đặc biệt khi kết hợp với hàm.

    def add(x, y): return x + y
    def subtract(x, y): return x - y
    def multiply(x, y): return x * y
    
    operations = {
        "+": add,
        "-": subtract,
        "*": multiply,
    }
    
    op_symbol = "-"
    x_val, y_val = 10, 5
    
    # Lấy hàm từ dict và gọi nó
    if op_symbol in operations:
        result = operations[op_symbol](x_val, y_val)
        print(f"Result of {x_val} {op_symbol} {y_val} = {result}") # Output: Result of 10 - 5 = 5
    else:
        print("Unknown operation")
    
    # Cách dùng get với hàm lambda cho trường hợp mặc định
    # default_operation = lambda x, y: print("Unknown operation")
    # result = operations.get(op_symbol, default_operation)(x_val, y_val)
    

    Giải thích: Dictionary operations ánh xạ các ký hiệu toán tử (key) tới các hàm thực hiện phép toán tương ứng (value). Điều này giúp tránh các chuỗi if/elif/else dài dòng và dễ mở rộng hơn.

  5. Caching/Memoization: Lưu trữ kết quả của các hàm tốn thời gian để tránh tính toán lại. Key là đầu vào của hàm, value là kết quả trả về. (Đây là một chủ đề nâng cao hơn, nhưng dictionary là nền tảng).

Việc hiểu rõ và áp dụng các kỹ thuật sử dụng dict hiệu quả không chỉ giúp tối ưu hiệu năng chương trình mà còn làm cho mã nguồn của bạn trở nên sạch sẽ, dễ đọc và dễ bảo trì hơn. Hãy luyện tập sử dụng chúng thường xuyên trong các dự án của bạn!

Comments

There are no comments at the moment.