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:
dict
là một tập hợp các cặp key-value (khóa-giá trị). Mỗikey
trong dictionary là duy nhất và được sử dụng để truy cậpvalue
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
[]
vàkey
để lấyvalue
tương ứng. Cần lưu ý rằng nếukey
khô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ếukey
chưa tồn tại, một cặpkey-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ếukey
khô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
value
dựa trênkey
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ủ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
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 xemkey
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ếukey
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ủakey
. Nếukey
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ắnkey
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ếukey
không tồn tại, nó sẽ thêm cặpkey: default
và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ộtkey
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áckey
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á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ảkey
vàvalue
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ế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.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()
).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/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ỗiif/elif/else
dà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