Bài 3.16. Sử dụng dict trong Python hiệu quả
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:
dictlà một tập hợp các cặp key-value (khóa-giá trị). Mỗikeytrong dictionary là duy nhất và được sử dụng để truy cậpvaluetươ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
keythay vì dựa vào thứ tự. Keyphải là các đối tượng immutable (không thể thay đổi), ví dụ: số, chuỗi, tuple.Valuecó 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 KeyErrorGiải thích: Sử dụng dấu ngoặc vuông
[]vàkeyđể lấyvaluetương ứng. Cần lưu ý rằng nếukeykhông tồn tại, chương trình sẽ dừng lại với lỗiKeyError. 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ậtvalue. Nếukeychưa tồn tại, một cặpkey-valuemớ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:
delxóa cặp key-value trực tiếp.pop(key)xóa cặp key-value và trả vềvaluetương ứng. Nếukeykhông tồn tại, cả hai đều gâyKeyError(trừ khipop()đượ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.
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
valuedựa trênkeycủ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ủakey) thường nhanh hơn rất nhiều so với tìm kiếm tronglist.Tính dễ đọc và cấu trúc: Sử dụng
keycó ý 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 xemkeycó 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: BobGiải thích:
key in dictionarytrả vềTruenếukeytồn tại,Falsenế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ủakey. Nếukeykhô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: CharlieGiả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ắnkeycó 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ếukeykhông tồn tại, nó sẽ thêm cặpkey: defaultvào dictionary trước khi trả vềdefault. Nếukeyđã 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ộtkeytồ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
fortrực tiếp trên dictionary sẽ duyệt qua cáckeycủ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ácvalue.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ảkeyvàvaluetrong 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 đổiGiải thích:
update()sửa đổi trực tiếpdict1. 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:
Đế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.Counterlà 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()).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 đó.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.Thay thế câu lệnh
switch/case(Dispatching): Python không có cấu trúcswitch/casenhư 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ỗiif/elif/elsedài dòng và dễ mở rộng hơn.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