Chuyển đến nội dung chính

Các phương thức Dictionary trong Python: Hướng dẫn đầy đủ

Làm chủ các phương thức dictionary trong Python từ khởi tạo và chỉnh sửa đến tối ưu nâng cao. Học cách xử lý lỗi và mở rộng hiệu năng cho quy trình sản xuất.
· 15 phút đọc

Khám phá cùng AI

Với bất kỳ nhà khoa học dữ liệu hay kỹ sư phần mềm nào, khả năng ánh xạ hiệu quả các mối quan hệ giữa các điểm dữ liệu là kỹ năng không thể thiếu. Dù bạn đang phân tích các phản hồi JSON phức tạp từ một API, tổng hợp thống kê từ một tập dữ liệu khổng lồ, hay đơn giản là cấu hình thiết lập ứng dụng, thì dictionary có lẽ là công cụ mạnh mẽ nhất của Python. Nó giúp thao tác dữ liệu sạch, dễ đọc và tối ưu cao.

Ai cũng có thể tra cứu một giá trị trong dictionary, nhưng chuyên môn thật sự thể hiện khi bạn biết vận dụng các phương thức của nó vào luồng công việc dữ liệu và mở khóa những mẫu hình nâng cao.

Trong bài viết này, chúng ta sẽ tìm hiểu về bảng băm (hash table) giúp dictionary nhanh đến vậy, các phương thức cốt lõi của dictionary, chiến lược xử lý lỗi và kỹ thuật tối ưu hiệu năng. 

Nếu bạn mới làm quen với dictionary, tôi khuyến nghị đọc bài hướng dẫn nền tảng Python Dictionary của chúng tôi như một điểm khởi đầu tốt.

Python Dictionary là gì?

Dictionary trong Python là một cấu trúc dữ liệu dựng sẵn được thiết kế cho tra cứu nhanh và linh hoạt. Chúng cho phép bạn lưu trữ và truy xuất giá trị bằng các khóa có ý nghĩa thay vì vị trí số, khiến chúng lý tưởng để biểu diễn dữ liệu có cấu trúc, gần với thế giới thực. Hãy xem cấu trúc lõi và các thuộc tính của chúng.

Kiến trúc cốt lõi của dictionary và hashing

Trước khi đi vào chi tiết các phương thức của dictionary trong Python, sẽ hữu ích nếu hiểu cách dictionary được xây dựng trên bảng băm. Nhiều lỗi bạn gặp phải, như TypeError: unhashable type, xuất phát trực tiếp từ cách cấu trúc này hoạt động.

Ở cấp độ cấu trúc, một dictionary trong Python triển khai bảng băm. Lựa chọn kiến trúc này mang lại tốc độ và tính linh hoạt cho dictionary. Khi bạn định nghĩa một dictionary, về bản chất bạn đang tạo một mảng thưa, thường được gọi là mảng bucket.

Khi bạn chèn một cặp khóa–giá trị, Python đưa khóa qua một hàm băm. Hàm này tính ra một số nguyên duy nhất (hash) xác định chỉ số cụ thể trong mảng bucket nơi giá trị sẽ được lưu.

Do thiết kế này:

  • Khóa phải có thể băm (hashable), thường có nghĩa là chúng phải thuộc kiểu bất biến (ví dụ, str, int, tuple)

  • Giá trị có thể là kiểu biến đổi, bao gồm list, dictionary khác, hoặc đối tượng tùy chỉnh

  • Tra cứu, chèn và xóa chạy với thời gian trung bình đã khấu hao O(1)

Ví dụ sau cho thấy một vài khóa hợp lệ, đồng thời minh họa rằng list không được chấp nhận làm khóa dictionary:

# Valid dictionary - immutable keys, any values
user_data = {
    "name": "Alice",           # string key, string value
    42: [1, 2, 3],             # integer key, list value
    (10, 20): {"nested": True} # tuple key, dict value
}
print(type(user_data), "valid dict")

# Invalid - will raise TypeError
try:
    invalid_dict = {[1, 2]: "value"}  # lists are not hashable
except TypeError as e:
    print(f"Error: {e}")
<class 'dict'> valid dict
Error: unhashable type: 'list'

Cấu trúc này quan trọng đối với hiệu năng. Trong khi tìm kiếm một phần tử trong list cần lặp qua từng phần tử, một phép toán O(n), thì truy xuất một giá trị từ dictionary trung bình là O(1). 

Điều này có nghĩa là tra cứu một ID người dùng trong tập 1 triệu người dùng mất xấp xỉ cùng khoảng thời gian như trong tập 10 người dùng. Hiểu sự khác biệt giữa các kiểu dữ liệu trong Python là chìa khóa để chọn đúng cấu trúc cho trường hợp sử dụng của bạn.

python dictionary methods

Sự phát triển về thứ tự và thuộc tính

Một thay đổi quan trọng trong lịch sử Python xảy ra ở phiên bản 3.7. Trước đó, dictionary được coi là tập hợp không có thứ tự, và việc lặp qua chúng có thể tạo ra các khóa theo chuỗi có vẻ ngẫu nhiên. Nếu bạn in một dictionary, các mục có thể xuất hiện theo thứ tự khác với lúc chèn, tùy thuộc vào giá trị băm và lịch sử mảng nội bộ.

Tuy nhiên, bắt đầu từ Python 3.6, dictionary bắt đầu giữ nguyên thứ tự chèn như một chi tiết triển khai trong CPython. Rồi từ Python 3.7 (và được đảm bảo chính thức trong đặc tả ngôn ngữ), dictionary giữ nguyên thứ tự chèn. 

Sự chuyển dịch từ ánh xạ không thứ tự sang có thứ tự này mang vài hệ quả quan trọng cho phát triển Python hiện đại. Chẳng hạn, tuần tự hóa JSON giờ cho ra kết quả có thể dự đoán, giúp việc gỡ lỗi dễ hơn và đảm bảo khả năng tái lập dữ liệu qua các lần chạy khác nhau. 

Nếu bạn làm việc với pipeline dữ liệu mà thứ tự quan trọng, như xử lý sự kiện chuỗi thời gian hoặc duy trì hệ phân cấp cấu hình, thì bảo đảm này loại bỏ cả một lớp lỗi tinh vi. Tiếp theo, hãy xem cách tạo dictionary.

Tạo một Python Dictionary

Dù việc tạo dictionary có vẻ đơn giản, phương pháp bạn chọn có thể ảnh hưởng đến cả khả năng đọc và hiệu năng mã. Python cung cấp nhiều cách khởi tạo, từ literal đơn giản đến kỹ thuật sinh chương trình nâng cao cho luồng công việc khoa học dữ liệu. Hãy xem những cách quan trọng nhất.

Literal với ngoặc nhọn

Cách phổ biến và được ưa thích nhất để tạo dictionary là dùng cú pháp ngoặc nhọn {}. Cú pháp literal này không chỉ dễ đọc hơn mà còn nhanh hơn các phương pháp thay thế. Python có thể tối ưu việc dựng bytecode trực tiếp mà không chịu chi phí gọi hàm. Dưới đây là mã minh họa một dictionary:

# Preferred: Literal syntax
user_profile = {
    "name": "Alice",
    "role": "Data Scientist",
    "active": True
}
user_profile
{'name': 'Alice', 'role': 'Data Scientist', 'active': True}

Hàm dựng dict()

Tuy nhiên, hàm dựng dict() là không thể thiếu trong một vài tình huống. Nó đóng vai trò bộ chuyển kiểu, cho phép bạn xây dựng dictionary từ các dãy tuple hoặc đối số từ khóa. Nó đặc biệt hữu ích trong các trường hợp sau: 

  • Khóa là các định danh Python hợp lệ, nhưng bạn muốn tránh việc đặt trong dấu nháy 
  • Bạn cần chuyển đổi các cấu trúc dữ liệu như list các cặp giá trị
# Using keyword arguments (cleaner for string keys)
config = dict(host="localhost", port=8080, debug=True)
print(config)

# Converting a list of tuples (common in data processing)
pairs = [("a", 1), ("b", 2), ("c", 3)]
lookup_table = dict(pairs)
print(lookup_table)
{'host': 'localhost', 'port': 8080, 'debug': True}
{'a': 1, 'b': 2, 'c': 3}

Dictionary comprehension

Với các kịch bản tạo dictionary phức tạp hơn, dictionary comprehension cung cấp cách ngắn gọn và hiệu quả để lọc, chuyển đổi, hoặc sinh cặp khóa–giá trị theo chương trình. Đây là kỹ thuật thiết yếu cho bất kỳ người làm dữ liệu nào cần xử lý và tái định hình dữ liệu một cách động.

Comprehension rất quan trọng cho các tác vụ như:

  • Đảo ngược một dictionary
  • Lọc bỏ các giá trị null khỏi tập dữ liệu

python dictionary comprehensions

Hãy xem cách tạo một dictionary comprehension dưới đây:

# Classic use case: Creating a squares map
squares = {x: x**2 for x in range(5)}
print(squares)

# Filtering data during creation
raw_data = {"a": 10, "b": None, "c": 5}
clean_data = {k: v for k, v in raw_data.items() if v is not None}
print(clean_data)
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
{'a': 10, 'c': 5}

Nếu muốn đào sâu hơn, tôi khuyến nghị bạn xem bài hướng dẫn Python Dictionary Comprehension của chúng tôi.

dict.fromkeys()

Một phương thức hữu ích khác để khởi tạo dictionary là dict.fromkeys(). Phương thức này tạo một dictionary mới với các khóa chỉ định và một giá trị chung. Nó thường được dùng để khởi tạo bộ đếm hoặc cờ trạng thái.

# Initialize multiple keys with the same default value
categories = ["electronics", "clothing", "food", "books"]
inventory = dict.fromkeys(categories, 0)
print(inventory) 

# Initialize with None for optional fields
user_fields = ["email", "phone", "address", "company"]
user_profile = dict.fromkeys(user_fields)
print(user_profile)
{'electronics': 0, 'clothing': 0, 'food': 0, 'books': 0}
{'email': None, 'phone': None, 'address': None, 'company': None}

Khi dùng .fromkeys() với đối tượng có thể thay đổi như list hoặc dictionary, tất cả khóa sẽ tham chiếu cùng một đối tượng trong bộ nhớ. Điều này tạo ra “tham chiếu dùng chung” có thể dẫn đến hành vi khó lường. Hãy xem ví dụ sau:

# DANGEROUS - all keys share the same list!
categories = ["A", "B", "C"]
wrong_way = dict.fromkeys(categories, [])
wrong_way["A"].append(1)
print(wrong_way) 

# CORRECT - use dictionary comprehension for independent lists
right_way = {cat: [] for cat in categories}
right_way["A"].append(1)
print(right_way)
{'A': [1], 'B': [1], 'C': [1]}
{'A': [1], 'B': [], 'C': []}

Ta thấy cùng một giá trị đã được chia sẻ giữa mọi khóa trong trường hợp đầu. Để tránh điều này, cần dùng dictionary comprehension để có các list độc lập.

Các phương thức Dictionary trong Python để Truy cập và Chỉnh sửa

Khi đã tạo dictionary, tương tác với dữ liệu bên trong là một trong những tác vụ lập trình thường nhật. Hãy xem một số cách sau.

Truy cập và lấy giá trị

Có vài cách khác nhau để truy cập giá trị.

Ký pháp ngoặc vuông

Cách trực tiếp nhất để lấy một giá trị từ dictionary là dùng ký pháp ngoặc vuông d[key], trả về giá trị liên quan nếu khóa tồn tại. Cách này lý tưởng khi bạn tin chắc khóa có trong dictionary. Dưới đây là mã để thực hiện:

product = {
    "name": "Laptop",
    "price": 1299.99,
    "stock": 45,
    "category": "Electronics"
}

# Direct access with brackets
print(product["name"]) 
print(product["price"]) 

# Attempting to access a non-existent key raises KeyError
try:
    print(product["manufacturer"])
except KeyError as e:
    print(f"Key not found: {e}")
Laptop
1299.99
Key not found: 'manufacturer'

Phương thức .get()

Để lấy an toàn khi chưa chắc khóa có tồn tại, phương thức .get() là giải pháp tinh gọn. Nó trả về None (hoặc giá trị mặc định chỉ định) thay vì ném ngoại lệ nếu khóa không tồn tại.

# Safe retrieval with .get()
manufacturer = product.get("manufacturer")
print(manufacturer)  # None

# Provide a custom default value
warranty = product.get("warranty", "No warranty information")
print(warranty)

# .get() is especially useful in data pipelines
customer_data = {"name": "John Doe", "email": "[email protected]"}

phone = customer_data.get("phone", "Not provided")
print(phone)

address = customer_data.get("address", "Not provided")
print(address)
None
No warranty information
Not provided
Not provided

Phương thức .setdefault()

Phương thức .setdefault() kết hợp việc lấy và chèn trong một thao tác. Nó lấy giá trị nếu khóa tồn tại, hoặc chèn một giá trị mặc định và trả về giá trị đó nếu khóa thiếu, rất phù hợp cho các mẫu tích lũy.

# Using .setdefault() for initialization and retrieval
page_visits = {}

# First visit to 'home' - inserts 0 and returns it
count = page_visits.setdefault("home", 0)
print(count) 
page_visits["home"] += 1

# Subsequent call returns existing value
count = page_visits.setdefault("home", 0)
print(count)

# Practical example: grouping items
inventory = [
    ("apple", "fruit"),
    ("carrot", "vegetable"),
    ("banana", "fruit"),
    ("broccoli", "vegetable")
]

grouped = {}
for item, category in inventory:
    grouped.setdefault(category, []).append(item)

print(grouped)
0
1
{'fruit': ['apple', 'banana'], 'vegetable': ['carrot', 'broccoli']}

Chỉnh sửa dictionary

Dictionary là động; bạn sẽ thường cần thêm hoặc xóa dữ liệu khi chương trình chạy. 

Thêm cặp khóa–giá trị với phương thức .update()

Thêm một cặp đơn giản chỉ là gán (d['new'] = 1), nhưng cho thao tác hàng loạt, phương thức .update() vượt trội. Nó nhận một dictionary khác hoặc một iterable các cặp khóa–giá trị, và trộn chúng vào đối tượng hiện có.

Hãy xem cách dùng phương thức .update():

# Simple assignment for single key-value pairs
user = {"username": "alice_2024", "role": "analyst"}
user["email"] = "[email protected]"  # Add new key
user["role"] = "senior_analyst"      # Update existing key

# Bulk update with .update()
user.update({"department": "Analytics", "level": 3})
print(user)

# Update from sequence of tuples
additional_info = [("projects", 12), ("rating", 4.8)]
user.update(additional_info)
print(user)

# Update with keyword arguments
user.update(active=True, certified=True)
print(user)
{'username': 'alice_2024', 'role': 'senior_analyst', 'email': '[email protected]', 'department': 'Analytics', 'level': 3}
{'username': 'alice_2024', 'role': 'senior_analyst', 'email': '[email protected]', 'department': 'Analytics', 'level': 3, 'projects': 12, 'rating': 4.8}
{'username': 'alice_2024', 'role': 'senior_analyst', 'email': '[email protected]', 'department': 'Analytics', 'level': 3, 'projects': 12, 'rating': 4.8, 'active': True, 'certified': True}

Để được hướng dẫn chi tiết về thêm mục, tôi khuyến nghị bạn xem hướng dẫn về Python Dictionary Append.

Xóa mục khỏi dictionary

Python cung cấp ba phương thức khác biệt để xóa phần tử trong dictionary, mỗi cái có hành vi và trường hợp sử dụng khác nhau:

  • .pop(key): Xóa khóa và trả về giá trị của nó. Hữu ích khi bạn cần dùng dữ liệu thêm một lần trước khi xóa.

  • .popitem(): Xóa và trả về cặp khóa–giá trị chèn cuối cùng (LIFO). Đây là lợi ích trực tiếp từ tính có thứ tự của dictionary hiện đại.

  • del d[key]: chỉ xóa khóa. Không trả về giá trị và hơi nhanh hơn nếu không cần giá trị trả về.

Hãy xem ví dụ cho các phương thức này:

Phương thức .pop():

scores = {"Alice": 95, "Bob": 87, "Carol": 92, "David": 78}

# .pop() - removes key and returns its value
alice_score = scores.pop("Alice")
print(alice_score)
print(scores) 
95
{'Bob': 87, 'Carol': 92, 'David': 78}

Phương thức .popitem():

# .popitem() - removes and returns last inserted pair (LIFO in Python 3.7+)
scores = {"Alice": 95, "Bob": 87, "Carol": 92, "David": 78}
last_item = scores.popitem()
print(last_item)
print(scores)
('David', 78)
{'Alice': 95, 'Bob': 87, 'Carol': 92}

del:

# del statement - removes key without returning value
scores = {"Alice": 95, "Bob": 87, "Carol": 92, "David": 78}
del scores["Bob"]
print(scores)
{'Alice': 95, 'Carol': 92, 'David': 78}

Làm rỗng dictionary với phương thức .clear()

Phương thức .clear() làm rỗng toàn bộ dictionary, để lại một đối tượng {} trống. Điều này khác với việc xóa chính biến đó. Đối tượng vẫn tồn tại trong bộ nhớ, chỉ là rỗng. Hãy xem cách hoạt động:

# .clear() - removes all items but keeps the dictionary object
scores = {"Alice": 95, "Bob": 87, "Carol": 92, "David": 78}
scores.clear()
print(scores)
print(type(scores))
{}
<class 'dict'>

Sự khác biệt giữa các phương thức này là quan trọng. Dùng .pop() khi bạn cần giá trị vừa xóa, .popitem() cho hành vi giống stack, del cho việc xóa đơn giản, và .clear() để đặt lại dictionary trong khi vẫn giữ danh tính của nó.

View và vòng lặp

Trong các phiên bản Python 2 cũ, các phương thức như .keys() trả về một list tĩnh. Trong các phiên bản Python hiện tại (3.x), chúng trả về đối tượng view. View là “cửa sổ” động vào dictionary. Nếu dictionary thay đổi, view phản ánh các thay đổi đó ngay lập tức mà không cần gọi lại.

Lặp qua .keys(), .values(), và .items()

Bạn có thể lặp qua khóa (.keys()), giá trị (.values()), hoặc cả hai đồng thời bằng .items(). Hãy xem các phương thức này qua ví dụ:

experiment = {
    "model": "RandomForest",
    "accuracy": 0.94,
    "precision": 0.91,
    "recall": 0.89
}

# .keys() returns a view of all keys
print(experiment.keys()) 

# .values() returns a view of all values
print(experiment.values())

# .items() returns (key, value) tuples - most commonly used
for metric, value in experiment.items():
    if isinstance(value, float):
        print(f"{metric}: {value:.2%}")
dict_keys(['model', 'accuracy', 'precision', 'recall'])
dict_values(['RandomForest', 0.94, 0.91, 0.89])
accuracy: 94.00%
precision: 91.00%
recall: 89.00%

Tính động của đối tượng view nghĩa là chúng tự động phản chiếu các thay đổi được thực hiện với dictionary sau khi view được tạo. Hãy xem ví dụ:

metrics = {"MAE": 0.23, "RMSE": 0.45}
keys_view = metrics.keys()
print(keys_view)

# Add new metric
metrics["R2"] = 0.87
print(keys_view)
dict_keys(['MAE', 'RMSE'])
dict_keys(['MAE', 'RMSE', 'R2'])

Giao và hợp

Một tính năng mạnh mẽ của view là hỗ trợ các phép toán tập hợp. Bạn có thể thực hiện trực tiếp trên view khóa để so sánh hai dictionary một cách hiệu quả bằng các toán tử sau:

  • Giao: &

  • Hợp: |

  • Hiệu: -

  • Hiệu đối xứng/XOR: ^

Hãy xem ví dụ:

dict1 = {"a": 1, "b": 2, "c": 3}
dict2 = {"b": 20, "c": 30, "d": 4}

# Find common keys (intersection)
common_keys = dict1.keys() & dict2.keys()
print(common_keys)

# Find all unique keys (union)
all_keys = dict1.keys() | dict2.keys()
print(all_keys)

# Find keys in dict1 but not in dict2 (difference)
unique_to_dict1 = dict1.keys() - dict2.keys()
print(unique_to_dict1)

# Symmetric difference - keys in either but not both
exclusive_keys = dict1.keys() ^ dict2.keys()
print(exclusive_keys)
{'b', 'c'}
{'b', 'c', 'd', 'a'}
{'a'}
{'d', 'a'}

Các phép toán tập hợp dựa trên view này tiết kiệm bộ nhớ hơn nhiều so với việc chuyển dictionary thành set một cách tường minh, đặc biệt khi làm việc với các tập dữ liệu lớn.

Tránh lỗi trong các phương thức Dictionary của Python

Vì dictionary thường được dùng như giao diện chính cho dữ liệu bên ngoài, như payload JSON từ API hoặc file cấu hình, chúng là nguồn lỗi thời gian chạy phổ biến. Viết mã vững chắc đòi hỏi nhiều hơn việc biết cách truy cập dữ liệu. Nó còn đòi hỏi biết cách xử lý khi dữ liệu vắng mặt. 

Lỗi dictionary phổ biến nhất là KeyError, xảy ra khi cố truy cập một khóa không tồn tại. Python cung cấp hai triết lý xử lý lỗi này: 

  • EAFP (Easier to Ask Forgiveness than Permission — Dễ xin tha thứ hơn xin phép) 
  • LBYL (Look Before You Leap — Nhìn trước khi nhảy)

EAFP: Quản lý KeyError và ngoại lệ

Mẫu EAFP được coi là “Pythonic” hơn và thường hiệu quả hơn khi khóa thường tồn tại, vì nó tránh kiểm tra thừa. Nó dùng khối try-except để xử lý lỗi sau khi xảy ra, giả định rằng các thao tác thường sẽ thành công. Hãy xem cách hoạt động:

# EAFP approach - try first, handle exceptions
user_data = {"username": "data_analyst", "email": "[email protected]"}

try:
    phone = user_data["phone"]
    print(f"Phone: {phone}")
except KeyError:
    print("Phone number not available")
    phone = None

# More sophisticated error handling with specific actions
config = {"host": "localhost", "port": 5432}

try:
    database = config["database"]
except KeyError:
    print("Warning: Database not specified, using default")
    database = "default_db"
    config["database"] = database  # Add missing configuration
Phone number not available
Warning: Database not specified, using default

Tuy nhiên, có những tình huống việc “fail to” (thất bại rõ ràng), để KeyError lan truyền, thực sự tốt hơn so với thất bại im lặng. Trong cách LBYL, bạn kiểm tra tường minh sự tồn tại của khóa trước khi truy cập. Hãy xem ví dụ:

# Critical configuration - fail loudly if missing
required_config = {"api_key": "secret123", "endpoint": "api.example.com"}

def initialize_api(config):
    # Don't catch KeyError - we WANT the program to crash if required keys are missing
    api_key = config["api_key"]
    endpoint = config["endpoint"]
    timeout = config.get("timeout", 30)  # Optional with default
    
    return {"key": api_key, "endpoint": endpoint, "timeout": timeout}

Gọi hàm này sẽ ném KeyError nếu thiếu bất kỳ khóa nào trong dictionary, điều này là hành vi đúng vì tốt hơn nên thất bại ngay khi khởi tạo hơn là im lặng trong sản xuất.

Xử lý lỗi tường minh

Khi xử lý dữ liệu từ nguồn bên ngoài như API hoặc đầu vào người dùng, xử lý lỗi tường minh trở nên quan trọng. Hãy xem ví dụ:

# Processing API response with defensive error handling
def extract_user_info(api_response):
    """Extract user information with comprehensive error handling."""
    user_info = {}
    
    try:
        user_info["id"] = api_response["user"]["id"]
        user_info["name"] = api_response["user"]["profile"]["name"]
    except KeyError as e:
        print(f"Missing required field in API response: {e}")
        return None
    
    # Optional fields - use .get() with defaults
    user_info["email"] = api_response.get("user", {}).get("contact", {}).get("email", "N/A")
    user_info["verified"] = api_response.get("user", {}).get("verified", False)
    
    return user_info

# Example usage
response = {
    "user": {
        "id": 12345,
        "profile": {"name": "Jane Smith"},
        "verified": True
    }
}

user = extract_user_info(response)
print(user)
{'id': 12345, 'name': 'Jane Smith', 'email': 'N/A', 'verified': True}

Hiểu khi nào dùng .get() so với ký pháp ngoặc vuông và so với try-except là quan trọng. 

LBYL: Kiểm tra thành viên chủ động

Cách LBYL dùng câu lệnh điều kiện với toán tử in và not in để kiểm tra tồn tại khóa trước khi truy cập. Mẫu này rõ ràng hơn khi xử lý logic điều kiện hoặc khi bạn cần thực hiện hành động khác nhau dựa trên sự hiện diện của khóa. Hãy xem ví dụ:

# Proactive checking with 'in' operator
student_grades = {
    "Alice": 95,
    "Bob": 87,
    "Carol": 92
}

# Check before access
student_name = "David"
if student_name in student_grades:
    print(f"{student_name}'s grade: {student_grades[student_name]}")
else:
    print(f"No grade recorded for {student_name}")

# Conditional update based on existence
if "David" not in student_grades:
    student_grades["David"] = 0  # Initialize new student
    print("New student added to grading system")

# Multiple key checks for validation
required_fields = ["name", "email", "department"]
employee_record = {"name": "John Doe", "email": "[email protected]"}

missing_fields = [field for field in required_fields if field not in employee_record]
if missing_fields:
    print(f"Error: Missing required fields: {missing_fields}")
else:
    print("All required fields present")
No grade recorded for David
New student added to grading system
Error: Missing required fields: ['department']

Khi xác thực dictionary có nguồn gốc từ bên ngoài như JSON từ API, file CSV, hoặc đầu vào người dùng, kiểm tra thành viên chủ động cung cấp logic xác thực rõ ràng, dễ đọc. Hãy xem ví dụ:

# Validating API response structure
def validate_product_data(product):
    """Validate product dictionary has all required fields."""
    required = ["id", "name", "price", "category"]
    optional = ["description", "stock", "manufacturer"]
    
    # Check all required fields exist
    for field in required:
        if field not in product:
            raise ValueError(f"Missing required field: {field}")
    
    # Validate data types for existing fields
    if "price" in product and not isinstance(product["price"], (int, float)):
        raise TypeError("Price must be a number")
    
    if "stock" in product and product["stock"] < 0:
        raise ValueError("Stock cannot be negative")
    
    return True

# Example usage with proper error handling
product_from_api = {
    "id": 101,
    "name": "Wireless Mouse",
    "price": 29.99,
    "category": "Electronics",
    "stock": 150
}

try:
    if validate_product_data(product_from_api):
        print("Product data validated successfully")
        # Proceed with processing
except (ValueError, TypeError) as e:
    print(f"Validation failed: {e}")
Product data validated successfully

Chọn cách tiếp cận phù hợp

Lựa chọn giữa EAFP và LBYL thường phụ thuộc trường hợp sử dụng. Dùng EAFP khi thao tác thường thành công và ngoại lệ hiếm. Dùng LBYL khi bạn cần rẽ nhánh tường minh hoặc khi xác thực đầu vào trước các thao tác tốn kém.

Trước khi lặp qua một dictionary lấy từ nguồn bên ngoài, thực hành tốt nhất là:

  • Xác thực sớm
  • Kiểm tra tất cả khóa bắt buộc một lần
  • Nêu rõ và cung cấp ngoại lệ có thông tin
  • Tách bạch rõ ràng trường bắt buộc và tùy chọn
  • Khi cần, xác thực hơn cả sự hiện diện
  • Ưu tiên lỗi tường minh hơn các giá trị dự phòng im lặng

Bằng cách theo các thực hành này, bạn có thể khiến mã vững chắc hơn nhiều khi xử lý dữ liệu không đoán trước được hoặc không đầy đủ từ API, đầu vào người dùng, hoặc file cấu hình.

Để tìm hiểu sâu hơn về viết mã Python bền bỉ, tôi khuyến nghị bạn học khóa Writing Efficient Python Code.

Các phương thức Dictionary nâng cao trong Python

Khi dự án khoa học dữ liệu của bạn trở nên phức tạp, bạn sẽ thường cần kết hợp, sao chép và chuyển đổi dictionary. Học các thao tác nâng cao này cho phép bạn thao tác cấu trúc dữ liệu hiệu quả đồng thời tránh các lỗi tinh vi có thể làm hỏng pipeline. Hãy xem một số phương thức sau.

Toán tử hợp nhất

Python 3.9 giới thiệu các toán tử hợp nhất tao nhã để trộn dictionary:

  • | (toán tử hợp): Tạo một dictionary mới đã trộn.

  • |= (toán tử cập nhật): Cập nhật dictionary hiện có tại chỗ.

Các toán tử này cung cấp cú pháp sạch, dễ đọc để kết hợp dữ liệu từ nhiều nguồn. Hãy xem ví dụ:

# Union operator | creates a new merged dictionary
defaults = {"theme": "light", "language": "en", "notifications": True}
user_prefs = {"theme": "dark", "font_size": 14}

final_config = defaults | user_prefs
print(final_config)

# Update operator |= modifies in place
settings = {"auto_save": True, "theme": "light"}
settings |= {"theme": "dark", "font_size": 12}
print(settings)
{'theme': 'dark', 'language': 'en', 'notifications': True, 'font_size': 14}
{'auto_save': True, 'theme': 'dark', 'font_size': 12}

Với các phiên bản Python trước 3.9, phương pháp giải nén hai dấu sao (**) cung cấp chức năng tương tự:

# Double-star unpacking
base_config = {"host": "localhost", "port": 5432, "ssl": False}
override_config = {"port": 5433, "ssl": True, "timeout": 30}

# Merge using unpacking
merged = {**base_config, **override_config}
print(merged)

# Multiple dictionary merge
db_config = {"database": "analytics"}
auth_config = {"username": "admin", "password": "secret"}
pool_config = {"pool_size": 10, "max_overflow": 20}

complete_config = {**db_config, **auth_config, **pool_config}
print(complete_config)
{'host': 'localhost', 'port': 5433, 'ssl': True, 'timeout': 30}
{'database': 'analytics', 'username': 'admin', 'password': 'secret', 'pool_size': 10, 'max_overflow': 20}

Độ ưu tiên khi va chạm

Với mọi kỹ thuật trộn, quy tắc đơn giản là: cái xuất hiện sau cùng sẽ thắng. Giá trị từ dictionary bên phải ghi đè cái bên trái nếu khóa trùng nhau:

dict1 = {"a": 1, "b": 2, "c": 3}
dict2 = {"b": 20, "c": 30, "d": 4}
dict3 = {"c": 300, "e": 5}

result = dict1 | dict2 | dict3
print(result)

result2 = {**dict1, **dict2, **dict3}
print(result2)

# Order matters - reversing changes the result
result3 = dict3 | dict2 | dict1
print(result3)
{'a': 1, 'b': 20, 'c': 300, 'd': 4, 'e': 5}
{'a': 1, 'b': 20, 'c': 300, 'd': 4, 'e': 5}
{'c': 3, 'e': 5, 'b': 2, 'd': 4, 'a': 1}

Hành vi này nhất quán dù bạn dùng |, |=, giải nén, hay phương thức .update(). Thứ tự rất quan trọng, đặc biệt hữu ích cho quản lý cấu hình phân tầng (ví dụ: mặc định hệ thống < cấu hình môi trường < ghi đè của người dùng).

python dictionary methodsfor merging

Cơ chế sao chép

Một cạm bẫy nguy hiểm là hiểu sai cách Python xử lý phép gán biến. 

Gán tham chiếu tạo bí danh, không phải bản sao

dict_a = dict_b không tạo bản sao. Nó tạo tham chiếu (bí danh). Sửa một cái sẽ sửa cái kia. Ví dụ sau minh họa khái niệm:

# Reference assignment - creates an alias, not a copy
original = {"name": "Dataset_v1", "records": 1000}
alias = original

# Modifying through the alias changes the original
alias["records"] = 2000
print(original)
print(alias is original)
{'name': 'Dataset_v1', 'records': 2000}
True

Như ta thấy, chỉ có một dictionary và cả original lẫn alias cùng tham chiếu đến nó. Do đó, khi giá trị cho khóa records được đặt thành 2000, thay đổi chỉ áp dụng cho một dictionary này.

Bản sao nông với .copy() hoặc .dict()

Bản sao nông (.copy() hoặc dict()) tạo một đối tượng dictionary mới, nhưng các đối tượng có thể thay đổi lồng bên trong vẫn là tham chiếu dùng chung.

# Shallow copy - creates a new dict but shares nested objects
original = {
    "name": "Experiment_A",
    "parameters": {"learning_rate": 0.01, "epochs": 100},
    "results": [0.85, 0.89, 0.92]
}

shallow = original.copy()

# Modifying top-level keys works as expected
shallow["name"] = "Experiment_B"
print(original["name"])
print(shallow["name"])

# But modifying nested objects affects both
shallow["parameters"]["learning_rate"] = 0.001
print(original["parameters"]["learning_rate"]) 

shallow["results"].append(0.94)
print(original["results"])
Experiment_A
Experiment_B
0.001
[0.85, 0.89, 0.92, 0.94]

Như bạn thấy, các khóa cấp cao được sửa như mong đợi, nhưng nếu bạn sửa các đối tượng có thể thay đổi lồng bên trong, nó ảnh hưởng đến cả bản gốc và bản sao nông.

Bản sao sâu dùng copy.deepcopy()

Vì vậy, bản sao nông thường gây lỗi tinh vi trong pipeline machine learning và quản lý cấu hình.

Do đó, bạn cần bản sao sâu bằng copy.deepcopy() từ thư viện chuẩn cho các dictionary chứa đối tượng có thể thay đổi lồng bên trong. Nó sao chép đệ quy mọi đối tượng lồng, tạo cấu trúc hoàn toàn độc lập. Ví dụ dưới đây minh họa:

import copy

# Deep copy - creates completely independent nested structures
original = {
    "model": "RandomForest",
    "hyperparameters": {
        "n_estimators": 100,
        "max_depth": 10,
        "min_samples_split": 2
    },
    "feature_importance": [0.3, 0.25, 0.2, 0.15, 0.1]
}

deep = copy.deepcopy(original)

# Modify nested structures
deep["hyperparameters"]["n_estimators"] = 200
deep["feature_importance"].append(0.05)

# Original remains completely unchanged
print(original["hyperparameters"]["n_estimators"])
print(len(original["feature_importance"]))
print(len(deep["feature_importance"]))
100
5
6

Thực hành tốt khi trộn và sao chép

Bằng cách áp dụng các kỹ thuật sau, bạn sẽ đảm bảo tính toàn vẹn dữ liệu và khả năng bảo trì trong các pipeline phức tạp.

  • Dùng | và |= cho các phép trộn dictionary sạch sẽ.

  • Nhớ rằng khóa thấy sau cùng sẽ thắng khi va chạm.

  • Phân biệt giữa gán tham chiếu, sao chép nông và sao chép sâu để tránh lỗi tinh vi.

  • Luôn ưu tiên copy.deepcopy() khi làm việc với cấu trúc lồng có thể thay đổi trong mã sản xuất.

Các kiểu Dictionary chuyên biệt trong Python

Dù dict chuẩn rất đa năng, thư viện chuẩn của Python cung cấp các ánh xạ chuyên biệt tối ưu cho tác vụ cụ thể như đếm, gom nhóm, hoặc áp đặt cấu trúc dữ liệu. Chọn đúng kiểu chuyên biệt có thể đơn giản hóa mã đáng kể và ngăn cả một lớp lỗi.

Các phần mở rộng của mô-đun collections

Mô-đun collections cung cấp một vài kiểu dictionary chuyên biệt hữu ích.

defaultdict

defaultdict từ mô-đun collections loại bỏ việc kiểm tra tồn tại khóa lặp lại bằng cách tự động khởi tạo khóa thiếu với giá trị mặc định. Điều này rất hữu ích cho các tác vụ tích lũy như đếm, gom nhóm, hoặc xây dựng cấu trúc lồng:

from collections import defaultdict

# Standard dict requires manual key checking
word_count = {}
text = "the quick brown fox jumps over the lazy dog".split()

for word in text:
    if word not in word_count:
        word_count[word] = 0
    word_count[word] += 1

# defaultdict eliminates the check
word_count_auto = defaultdict(int)  # int() returns 0
for word in text:
    word_count_auto[word] += 1  # No checking needed!

print(dict(word_count_auto))

# Grouping with defaultdict(list)
transactions = [
    ("2024-01-15", "groceries", 45.50),
    ("2024-01-15", "gas", 60.00),
    ("2024-01-16", "groceries", 32.75),
    ("2024-01-16", "entertainment", 25.00),
    ("2024-01-17", "gas", 55.00)
]

by_date = defaultdict(list)
for date, category, amount in transactions:
    by_date[date].append((category, amount))

for date, items in by_date.items():
    print(f"{date}: {items}")

# Nested defaultdict for complex structures
nested = defaultdict(lambda: defaultdict(int))
events = [
    ("2024-01", "login", 150),
    ("2024-01", "purchase", 45),
    ("2024-02", "login", 200),
    ("2024-02", "purchase", 60)
]

for month, event_type, count in events:
    nested[month][event_type] += count

print(dict(nested))
{'the': 2, 'quick': 1, 'brown': 1, 'fox': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1}
2024-01-15: [('groceries', 45.5), ('gas', 60.0)]
2024-01-16: [('groceries', 32.75), ('entertainment', 25.0)]
2024-01-17: [('gas', 55.0)]
{'2024-01': defaultdict(<class 'int'>, {'login': 150, 'purchase': 45}), '2024-02': defaultdict(<class 'int'>, {'login': 200, 'purchase': 60})}

Counter

Lớp Counter là một lớp con dictionary chuyên biệt được thiết kế để đếm các đối tượng có thể băm và thực hiện các phép toán đa tập hợp. Nó đặc biệt mạnh cho phân tích thống kê và phân phối tần suất. Hãy xem ví dụ cách hoạt động:

from collections import Counter

# Count occurrences in a sequence
tags = ["python", "data", "python", "ml", "data", "python", "statistics", "ml"]
tag_counts = Counter(tags)
print(tag_counts)

# Most common elements
print(tag_counts.most_common(2))

# Counter arithmetic - multiset operations
skills_alice = Counter(["Python", "SQL", "Tableau", "Python"])
skills_bob = Counter(["Python", "R", "SQL"])

# Union (maximum of counts)
combined_skills = skills_alice | skills_bob
print(combined_skills)

# Intersection (minimum of counts)
shared_skills = skills_alice & skills_bob
print(shared_skills)

# Addition (sum of counts)
total_mentions = skills_alice + skills_bob
print(total_mentions)

# Practical example: Analyzing survey responses
responses = ["satisfied", "neutral", "satisfied", "satisfied", 
             "dissatisfied", "neutral", "satisfied", "very_satisfied"]
sentiment_analysis = Counter(responses)

# Calculate percentage distribution
total = sum(sentiment_analysis.values())
for sentiment, count in sentiment_analysis.most_common():
    percentage = (count / total) * 100
    print(f"{sentiment}: {count} ({percentage:.1f}%)")
Counter({'python': 3, 'data': 2, 'ml': 2, 'statistics': 1})
[('python', 3), ('data', 2)]
Counter({'Python': 2, 'SQL': 1, 'Tableau': 1, 'R': 1})
Counter({'Python': 1, 'SQL': 1})
Counter({'Python': 3, 'SQL': 2, 'Tableau': 1, 'R': 1})
satisfied: 4 (50.0%)
neutral: 2 (25.0%)
dissatisfied: 1 (12.5%)
very_satisfied: 1 (12.5%)

OrderedDict

Trước Python 3.7, OrderedDict là thiết yếu để giữ thứ tự chèn. Dù dictionary chuẩn giờ duy trì thứ tự, OrderedDict vẫn có các trường hợp dùng cụ thể, đặc biệt cho so sánh bình đẳng có xét thứ tự và để di chuyển phần tử về hai đầu:

from collections import OrderedDict

# OrderedDict equality considers order
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 2, "a": 1}
print(dict1 == dict2)

ordered1 = OrderedDict([("a", 1), ("b", 2)])
ordered2 = OrderedDict([("b", 2), ("a", 1)])
print(ordered1 == ordered2)

# Move items to beginning or end
task_queue = OrderedDict([
    ("task1", "pending"),
    ("task2", "in_progress"),
    ("task3", "pending")
])

# Move task3 to the beginning (highest priority)
task_queue.move_to_end("task3", last=False)
print(list(task_queue.keys()))

# Move task2 to the end (lowest priority)
task_queue.move_to_end("task2")
print(list(task_queue.keys()))
True
False
['task3', 'task1', 'task2']
['task3', 'task1', 'task2']

Biểu đồ sau minh họa khi nào nên chọn kiểu dictionary nào.

When to use which Python dictionary type

An toàn kiểu hiện đại và view chỉ đọc

Khi Python được sử dụng rộng rãi trong môi trường sản xuất quy mô lớn, an toàn kiểu trở thành vấn đề quan trọng. Mô-đun typing và types giới thiệu cách áp đặt cấu trúc lên dictionary của bạn. Hiểu về các phương thức dunder trong Python cũng có thể giúp bạn xây dựng các đối tượng giống dictionary với hành vi đặc biệt.

TypedDict

Mô-đun typing của Python cung cấp TypedDict để định nghĩa cấu trúc dictionary với các khóa bắt buộc cụ thể và chú thích kiểu. Điều này cải thiện tài liệu mã, bật gợi ý IDE và bắt lỗi kiểu trong phân tích tĩnh. Hãy xem cách hoạt động:

from typing import TypedDict

# Define strict structure for user data
class UserProfile(TypedDict):
    user_id: int
    username: str
    email: str
    is_active: bool
    role: str

# Create properly typed dictionary
user: UserProfile = {
    "user_id": 12345,
    "username": "data_scientist",
    "email": "[email protected]",
    "is_active": True,
    "role": "analyst"
}

# IDE will autocomplete and type-check
def process_user(user: UserProfile) -> str:
    # IDE knows these keys exist and their types
    return f"User {user['username']} (ID: {user['user_id']}) - {user['role']}"

# Optional keys with total=False
class PartialConfig(TypedDict, total=False):
    host: str
    port: int
    database: str  # All keys are optional

config: PartialConfig = {"host": "localhost"}  # Valid - partial config

MappingProxyType

Trong các tình huống bạn cần phơi bày một dictionary nhưng ngăn chỉnh sửa, MappingProxyType tạo một view bất biến, chỉ đọc của dictionary chuẩn. Điều này rất phù hợp để bảo vệ hằng số cấu hình toàn cục khỏi thay đổi vô tình. Hãy xem thực tế:

from types import MappingProxyType

# Create read-only view of configuration
_INTERNAL_CONFIG = {
    "API_VERSION": "v2",
    "MAX_RETRIES": 3,
    "TIMEOUT": 30,
    "ENDPOINTS": {
        "users": "/api/v2/users",
        "data": "/api/v2/data"
    }
}

# Expose as immutable proxy
CONFIG = MappingProxyType(_INTERNAL_CONFIG)

# Reading works normally
print(CONFIG["API_VERSION"]) 
print(CONFIG["TIMEOUT"]) 

# Modifications raise TypeError
try:
    CONFIG["TIMEOUT"] = 60
except TypeError as e:
    print(f"Cannot modify: {e}")

# Caution: nested modifications succeed
try:
    CONFIG["ENDPOINTS"]["users"] = "/new/endpoint"
except TypeError as e:
    print(f"Cannot modify nested: {e}")

# Practical use case: Class constants
class DataPipeline:
    _default_config = {
        "batch_size": 1000,
        "parallel_workers": 4,
        "retry_failed": True
    }
    
    # Expose as read-only to prevent accidental changes
    DEFAULT_CONFIG = MappingProxyType(_default_config)
    
    def __init__(self, custom_config=None):
        # Merge with custom config while keeping defaults safe
        self.config = {**self.DEFAULT_CONFIG, **(custom_config or {})}
v2
30
Cannot modify: 'mappingproxy' object does not support item assignment

Vì vậy, như ta thấy, MappingProxyType khiến chính dictionary chỉ đọc (không thêm, không xóa, không gán lại khóa), nhưng nó không “đóng băng” các giá trị có thể thay đổi bên trong. Để ngăn thay đổi cấu trúc lồng, bạn cũng phải làm bất biến các đối tượng lồng hoặc bọc chúng bằng view chỉ đọc riêng.

Chữ ký hàm và gợi ý kiểu

Với chữ ký hàm và gợi ý kiểu, hãy dùng Dict và Mapping từ mô-đun typing để mô tả cấu trúc dictionary kỳ vọng. Ta có thể làm như sau:

from typing import Dict, List, Mapping, Any

# Dict for mutable dictionaries
def process_scores(scores: Dict[str, float]) -> Dict[str, str]:
    """Convert numeric scores to letter grades."""
    grades = {}
    for student, score in scores.items():
        if score >= 90:
            grades[student] = "A"
        elif score >= 80:
            grades[student] = "B"
        elif score >= 70:
            grades[student] = "C"
        else:
            grades[student] = "F"
    return grades

# Mapping for read-only or general mapping types
def display_config(config: Mapping[str, Any]) -> None:
    """Display configuration - accepts any mapping type."""
    for key, value in config.items():
        print(f"{key}: {value}")

# Works with dict, MappingProxyType, OrderedDict, etc.
display_config({"host": "localhost", "port": 5432})
display_config(CONFIG)  # MappingProxyType from earlier
host: localhost
port: 5432
API_VERSION: v2
MAX_RETRIES: 3
TIMEOUT: 30
ENDPOINTS: {'users': '/new/endpoint', 'data': '/api/v2/data'}

Tối ưu hiệu năng các phương thức Dictionary trong Python 

Khi tập dữ liệu của bạn tăng từ hàng nghìn lên hàng triệu bản ghi, hiệu quả mã trở nên quan trọng. Vì lý do này, cần hiểu đặc tính hiệu năng của dictionary. Vậy nên, hãy xem độ phức tạp tính toán, hệ quả bộ nhớ, và chiến lược tối ưu cho các thao tác dictionary.

Phân tích độ phức tạp thời gian

Các thao tác dictionary đạt tốc độ cao nhờ triển khai bảng băm, mang lại độ phức tạp thời gian trung bình O(1) cho ba thao tác phổ biến: 

  • Truy xuất giá trị (get)
  • Chèn hoặc cập nhật mục (set)
  • Xóa mục (delete)

Hiệu năng thời gian hằng số này đồng nghĩa các thao tác này trung bình mất cùng bậc thời gian, dù dictionary có 10 hay 10 triệu phần tử. Hãy xem ví dụ mã:

import time
import statistics


def benchmark_lookup(size, repeats=50_000):
    """
    Measure the median time for a single dictionary lookup in a dictionary of given size.
    
    Parameters:
        size (int): Number of elements in the dictionary to create
        repeats (int): How many times to repeat the lookup (for more stable median)
    
    Returns:
        float: Median lookup time in microseconds (μs)
    """
    # Create a large dictionary with string keys and integer values
    large_dict = {f"key_{i}": i for i in range(size)}
    
    # The key we will look up repeatedly (last element)
    target_key = f"key_{size - 1}"
    
    # Store individual measurement times (in nanoseconds)
    times = []
    
    # Perform many lookups to reduce measurement noise
    for _ in range(repeats):
        # Use high-resolution timer (nanoseconds)
        start = time.perf_counter_ns()
        _ = large_dict[target_key]          # The actual dictionary lookup
        end = time.perf_counter_ns()
        times.append(end - start)
    
    # Calculate median time to minimize impact of outliers
    median_ns = statistics.median(times)
    
    # Convert nanoseconds to microseconds
    return median_ns / 1000


# Sizes to test (from 100k to 10 million elements)
sizes = [100_000, 1_000_000, 10_000_000]

print("Dictionary lookup benchmark (median time over many repeats)\n")
print(f"{'Size':>12} | {'Median Lookup Time':>18} | Notes")
print("-" * 50)

for size in sizes:
    lookup_time_us = benchmark_lookup(size)
    print(f"{size:>12,} | {lookup_time_us:>15.2f} μs   | "
          f"{'→ still ~constant' if size == sizes[-1] else ''}")
Dictionary lookup benchmark (median time over many repeats)

        Size | Median Lookup Time | Notes
--------------------------------------------------
     100,000 |            0.14 μs   | 
   1,000,000 |            0.14 μs   | 
  10,000,000 |            0.14 μs   | → still ~constant

Thời gian tra cứu trung vị ở trên có thể thay đổi tùy tài nguyên tính toán của bạn. Điểm mấu chốt là dù giá trị thế nào thì chúng gần như giữ nguyên với một chút biến thiên, khiến khác biệt hoàn toàn không đáng kể, chứng minh tính chất O(1).

Trung bình so với trường hợp xấu nhất

Tuy nhiên, trong trường hợp xấu nhất, độ phức tạp có thể suy giảm về O(n) khi xảy ra va chạm hàm băm quá mức. Va chạm băm xảy ra khi các khóa khác nhau tạo ra cùng giá trị hash, buộc Python phải tìm qua nhiều mục cùng bucket hash. Hãy xem ví dụ:

# Pathological case: forcing hash collisions
class BadHash:
    """Object with intentionally poor hash function."""
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return 1  # All instances hash to same value - worst case!
    
    def __eq__(self, other):
        return isinstance(other, BadHash) and self.value == other.value

# This will have O(n) performance due to collisions
bad_dict = {BadHash(i): i for i in range(1000)}

# Compare with well-distributed hashes
good_dict = {f"key_{i}": i for i in range(1000)}

# Benchmark the difference
start = time.perf_counter()
_ = bad_dict[BadHash(999)]
bad_time = time.perf_counter() - start

start = time.perf_counter()
_ = good_dict["key_999"]
good_time = time.perf_counter() - start

print(f"Bad hash lookup: {bad_time * 1_000_000:.2f} μs")
print(f"Good hash lookup: {good_time * 1_000_000:.2f} μs")
print(f"Performance degradation: {bad_time / good_time:.1f}x slower")
Bad hash lookup: 582.48 μs
Good hash lookup: 93.99 μs
Performance degradation: 6.2x slower

Một lần nữa, kết quả chính xác và mức suy giảm hiệu năng sẽ thay đổi theo sức mạnh tính toán của bạn. Nhưng xu hướng sẽ giống nhau: tra cứu với hàm băm kém sẽ lâu hơn đáng kể so với trường hợp đạt thời gian O(1).

Tự động đổi kích thước dictionary

Một chi phí ẩn khác là đổi kích thước. Khi dictionary lớn dần, Python tự động thay đổi kích thước bảng băm nội bộ để duy trì hiệu năng. Thao tác này tốn chi phí tính toán vì cần băm lại tất cả khóa hiện có và phân phối lại chúng trên mảng lớn hơn. 

Python dùng chiến lược hệ số tăng trưởng, thường ít nhất là gấp đôi kích thước khi đạt ngưỡng. Hiểu các mẫu đổi kích thước này giúp ích khi khởi tạo dictionary lớn. Nếu bạn biết kích thước cuối gần đúng, bạn có thể “tiền cấp phát” để tránh nhiều lần đổi kích thước.

Quản lý bộ nhớ và định cỡ

Tốc độ thường đi kèm chi phí bộ nhớ. Dictionary có chi phí bộ nhớ đáng kể so với tuple hoặc list vì chúng phải lưu cấu trúc bảng băm (chỉ mục, hash, khóa và giá trị). Hãy hiểu với ví dụ mã:

import sys

# Compare memory footprint of different data structures
data_list = [("name", "Alice"), ("age", 30), ("city", "NYC")]
data_tuple = (("name", "Alice"), ("age", 30), ("city", "NYC"))
data_dict = {"name": "Alice", "age": 30, "city": "NYC"}

print(f"List of tuples: {sys.getsizeof(data_list)} bytes")
print(f"Tuple of tuples: {sys.getsizeof(data_tuple)} bytes")
print(f"Dictionary: {sys.getsizeof(data_dict)} bytes")
List of tuples: 88 bytes
Tuple of tuples: 64 bytes
Dictionary: 184 bytes

Dùng __slots__ để giảm kích thước đối tượng

Trong ví dụ trên, bạn có thể thấy dictionary dùng ~2x–3x bộ nhớ cho cùng dữ liệu. Với các lớp tạo nhiều thể hiện, dùng __slots__ thay vì __dict__ của thể hiện có thể giảm đáng kể mức tiêu thụ bộ nhớ. 

Mặc định, Python lưu thuộc tính thể hiện trong dictionary truy cập qua __dict__, nhưng __slots__ dùng cấu trúc dạng mảng gọn hơn. Hãy xem ví dụ:

import sys

# Regular class - uses __dict__ for attributes
class RegularUser:
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email

# Optimized class - uses __slots__
class OptimizedUser:
    __slots__ = ['user_id', 'name', 'email']
    
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email

# Create instances
regular = RegularUser(12345, "Alice", "[email protected]")
optimized = OptimizedUser(12345, "Alice", "[email protected]")

# Compare memory usage
print(f"Regular instance: {sys.getsizeof(regular.__dict__)} bytes (__dict__)")
print(f"Optimized instance: {sys.getsizeof(optimized)} bytes (__slots__)")

# For massive instance counts, the savings multiply
regular_users = [RegularUser(i, f"User{i}", f"user{i}@example.com") for i in range(1000)]
optimized_users = [OptimizedUser(i, f"User{i}", f"user{i}@example.com") for i in range(1000)]

regular_total = sum(sys.getsizeof(u.__dict__) for u in regular_users)
optimized_total = sum(sys.getsizeof(u) for u in optimized_users)

print(f"\n1000 regular instances: {regular_total:,} bytes")
print(f"1000 optimized instances: {optimized_total:,} bytes")
print(f"Memory savings: {((regular_total - optimized_total) / regular_total * 100):.1f}%")
Regular instance: 296 bytes (__dict__)
Optimized instance: 56 bytes (__slots__)

1000 regular instances: 96,000 bytes
1000 optimized instances: 56,000 bytes
Memory savings: 41.7%

Dictionary của Python so với pandas DataFrame

Cuối cùng, dù dictionary rất tốt cho truy cập ngẫu nhiên, chúng không được tối ưu cho xử lý dữ liệu dạng cột. Nếu bạn xử lý dữ liệu dạng bảng quy mô lớn (ví dụ, hàng triệu dòng), chuyển sang pandas DataFrame là lựa chọn khôn ngoan, vì chúng được tối ưu cho cả hiệu quả bộ nhớ và tốc độ vector hóa. 

import pandas as pd
import time
import sys

n = 10_000

# Dict
dict_data = {i: {"user_id": i, "score": i * 1.5, "category": f"cat_{i % 10}"} for i in range(n)}

# Optimized DF: use category dtype for strings, int32 for ids
df_data = pd.DataFrame({
    "user_id": pd.Series(range(n), dtype="int32"),
    "score": [i * 1.5 for i in range(n)],
    "category": pd.Series([f"cat_{i % 10}" for i in range(n)], dtype="category")
})

dict_memory = sys.getsizeof(dict_data)
df_memory = df_data.memory_usage(deep=True).sum()

print(f"Dictionary: {dict_memory:,} bytes")
print(f"Optimized DataFrame: {df_memory:,} bytes")

# Bulk operation: mean score per category
start = time.perf_counter()
df_mean = df_data.groupby("category")["score"].mean()
df_time = (time.perf_counter() - start) * 1_000_000

# Equivalent in dict (manual loop)
start = time.perf_counter()
from collections import defaultdict
means = defaultdict(lambda: [0, 0])
for row in dict_data.values():
    cat = row["category"]
    means[cat][0] += row["score"]
    means[cat][1] += 1
dict_mean = {k: s/c for k, (s, c) in means.items()}
dict_time = (time.perf_counter() - start) * 1_000_000

print(f"\nDF groupby mean: {df_time:.2f} μs")
print(f"Dict manual mean: {dict_time:.2f} μs")
Dictionary: 294,992 bytes
Optimized DataFrame: 130,972 bytes

DF groupby mean: 1630.24 μs
Dict manual mean: 3931.47 μs

Bạn có thể thấy rõ DataFrame vượt trội so với dictionary trong ví dụ này. Với luồng công việc khoa học dữ liệu cần cả tra cứu nhanh và thao tác phân tích, hãy cân nhắc cách tiếp cận kết hợp như dùng dictionary cho lập chỉ mục và truy cập nhanh, rồi chuyển sang DataFrame cho phân tích hàng loạt. 

Chìa khóa của tối ưu là khớp cấu trúc dữ liệu với mẫu truy cập của bạn. Nếu bạn thường xuyên tra cứu theo khóa, dictionary là tối ưu. Với thao tác dạng cột, lọc và tổng hợp trên tập dữ liệu lớn, DataFrame mang lại hiệu năng tốt.

Kết luận

Dictionary không chỉ là các thùng chứa đơn giản. Chúng là “chất keo” gắn kết các ứng dụng khoa học dữ liệu phức tạp. Dù bạn đang xây dựng bảng tra cứu nhanh cho một script hay kiến trúc một pipeline dữ liệu thông lượng cao, dictionary có khả năng là công cụ giá trị nhất của bạn.

Tuy nhiên, dựa dẫm vào hành vi mặc định mà không có xử lý lỗi vững chắc là công thức dẫn tới lỗi thời gian chạy. Các mẫu xử lý lỗi bền bỉ, như dùng EAFP so với LBYL và xác thực chủ động, có khả năng ngăn thất bại thời gian chạy khi xử lý dữ liệu bên ngoài. 

Các collection chuyên biệt như defaultdict, Counter, và TypedDict đưa mã của bạn từ mức “chạy được” lên “đạt chuẩn sản xuất”. Luôn ghi nhớ độ phức tạp thời gian và quản lý bộ nhớ để đảm bảo mã chạy hiệu quả. Cũng hãy nhớ rằng tối ưu là một quá trình lặp.

Để tiếp tục xây dựng kỹ năng Python, tôi khuyến nghị đăng ký khóa Intermediate Python để học thêm về cấu trúc dữ liệu, hoặc lộ trình nghề nghiệp tổng quát hơn Python Developer để có lộ trình học toàn diện.

Python Dictionary Methods FAQs

Ưu điểm của việc dùng phương thức .get() thay vì ký pháp ngoặc vuông là gì?

Phương thức .get() an toàn trả về None (hoặc giá trị mặc định tùy chọn) nếu thiếu khóa, trong khi ký pháp ngoặc vuông d[key] sẽ làm chương trình bạn bị lỗi với KeyError.

Phương thức .setdefault() hoạt động như thế nào?

Nó kết hợp truy xuất và chèn trong một bước. Nếu khóa tồn tại, nó trả về giá trị hiện có. Nếu khóa thiếu, nó chèn khóa với giá trị mặc định bạn chỉ định và trả về giá trị mới đó.

Sự khác nhau giữa .pop() và .popitem() là gì?

Phương thức .pop(key) xóa một khóa cụ thể và trả về giá trị của nó. Phương thức .popitem() xóa và trả về cặp khóa–giá trị được chèn gần nhất, theo thứ tự LIFO (vào sau ra trước).

Cách hiệu quả nhất để gộp nhiều mục mới vào một dictionary là gì?

Hãy dùng phương thức .update(). Nó cho phép bạn thêm hàng loạt hoặc ghi đè nhiều cặp khóa–giá trị cùng lúc bằng cách truyền vào một dictionary khác, một iterable các tuple, hoặc đối số từ khóa.

Kết quả trả về bởi .keys(), .values(), và .items() có phải là các list tĩnh không?

Không. Trong Python hiện đại (3.x), các phương thức này trả về đối tượng view động. Nếu bạn thêm hoặc xóa mục khỏi dictionary, các view này sẽ phản ánh thay đổi ngay lập tức mà không cần gọi lại.

Tác giả

Rajesh Kumar

Tôi là một biên tập viên nội dung về khoa học dữ liệu. Tôi yêu thích sáng tạo nội dung xoay quanh các chủ đề AI/ML/DS. Tôi cũng khám phá các công cụ AI mới và viết về chúng.

Chủ đề