•9 min read

Cấu trúc dữ liệu Python nâng cao: Ngừng dùng List và Dict cho mọi thứ

Cấu trúc dữ liệu Python nâng cao: Ngừng dùng List và Dict cho mọi thứ

Hầu hết các nhà phát triển Python đều mặc định sử dụng hai cấu trúc dữ liệu cơ bản cho hầu hết mọi vấn đề kỹ thuật: list chung và dict tiêu chuẩn.

Mặc dù các triển khai danh sách và từ điển tích hợp của Python là những kỳ công của kỹ thuật C, việc coi chúng là công cụ vạn năng sẽ dẫn đến suy giảm hiệu suất nghiêm trọng khi tập dữ liệu mở rộng. Sử dụng list làm hàng đợi nhập trước xuất trước (FIFO) biến một thao tác lẽ ra chỉ mất nano giây thành một dịch chuyển bộ nhớ O(n) làm đình trệ backend của bạn. Tương tự, việc duy trì các từ điển lồng nhau phức tạp để theo dõi đối tượng tiêu tốn bộ nhớ quá mức do các từ điển thuộc tính động.

Thư viện chuẩn Python bao gồm các cấu trúc dữ liệu chuyên biệt, được tối ưu hóa bằng C trong các mô-đun collections, heapq và dataclasses giúp cải thiện đáng kể tốc độ thực thi và giảm mức tiêu thụ bộ nhớ.

Trong hướng dẫn này, chúng ta sẽ khám phá cơ chế sản xuất của Counter, deque, defaultdict, heapq và các dataclass có khe cắm được tối ưu hóa bộ nhớ.


Advanced Python Data Structures

Audio Briefing
0:00 / 0:00

1. Hàng đợi FIFO và Cửa sổ trượt: collections.deque

Một list của Python được triển khai bên dưới dưới dạng một mảng động gồm các con trỏ bộ nhớ liền kề. Thêm vào cuối danh sách rất nhanh (trung bình O(1)), nhưng loại bỏ hoặc chèn từ đầu (list.pop(0)) yêu cầu thời gian chạy Python dịch chuyển mọi con trỏ tiếp theo trong bộ nhớ đi một vị trí—một thao tác O(n) tốn kém:

# ❌ ANTI-PATTERN: List as a FIFO queue (Catastrophic O(n) performance)
queue = []
for i in range(100_000):
    queue.append(i)

while queue:
    item = queue.pop(0) # Forces 100,000 pointer memory shifts!

Giải pháp: collections.deque

deque (hàng đợi hai đầu) được triển khai trong C dưới dạng một danh sách liên kết đôi gồm các khối có kích thước cố định (64 phần tử mỗi khối). Việc loại bỏ và thêm từ cả hai đầu được đảm bảo có độ phức tạp thời gian O(1):

# ✅ OPTIMAL: O(1) appends and pops from both ends
from collections import deque

queue = deque()
for i in range(100_000):
    queue.append(i)

while queue:
    item = queue.popleft() # Instantaneous O(1) pointer adjustment
Python Deque Illustration

Sức mạnh sản xuất: Cửa sổ trượt kích thước cố định với maxlen

Khi xử lý các luồng đo từ xa thời gian thực, bạn thường chỉ muốn giữ lại N điểm dữ liệu cuối cùng (ví dụ: tính toán trung bình động). Nếu bạn truyền maxlen, deque sẽ tự động loại bỏ các mục cũ hơn từ đầu đối diện khi các phần tử mới đến:

# Automatic rolling buffer of the last 5 events
recent_events = deque(maxlen=5)

for event_id in range(10):
    recent_events.append(f"event_{event_id}")

print(list(recent_events))
# Outputs: ['event_5', 'event_6', 'event_7', 'event_8', 'event_9']

Advertisement

2. Đa tập hợp và Theo dõi tần suất: collections.Counter

Theo dõi tần suất mục bằng cách sử dụng các vòng lặp từ điển thủ công gây nhiễu, dễ lỗi và chậm:

# ❌ The manual, verbose approach
counts = {}
for word in log_stream:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

Counter là một lớp con từ điển được tối ưu hóa bằng C được thiết kế đặc biệt để đếm các phần tử. Nó khởi tạo trong một lần chạy duy nhất trong C, không bao giờ gây ra KeyError (trả về 0 cho các khóa bị thiếu) và hỗ trợ toán học đa tập hợp:

from collections import Counter

# Instant O(n) frequency tabulation
word_counts = Counter(['apple', 'banana', 'apple', 'cherry', 'apple', 'banana'])

# Retrieve top 2 most common items instantly
print(word_counts.most_common(2))
# Outputs: [('apple', 3), ('banana', 2)]

# Mathematical set operations between Counters
batch_a = Counter(error=5, warning=2)
batch_b = Counter(error=3, warning=4, critical=1)

combined = batch_a + batch_b
print(combined)
# Outputs: Counter({'error': 8, 'warning': 6, 'critical': 1})
Python Counter Illustration

3. Danh sách kề và Nhóm: collections.defaultdict

Khi xây dựng đồ thị, bảng định tuyến hoặc nhóm các hàng cơ sở dữ liệu quan hệ theo khóa ngoại, các nhà phát triển thường viết các kiểm tra sự tồn tại lặp đi lặp lại:

# ❌ Repetitive dictionary checks
grouped_orders = {}
for order in orders:
    customer_id = order['customer_id']
    if customer_id not in grouped_orders:
        grouped_orders[customer_id] = []
    grouped_orders[customer_id].append(order)

defaultdict nhận một hàm tạo có thể gọi làm đối số đầu tiên của nó (chẳng hạn như list, set hoặc int) và tự động gọi nó bất cứ khi nào một khóa không tồn tại được truy cập:

# ✅ Clean and fast grouping
from collections import defaultdict

grouped_orders = defaultdict(list)
for order in orders:
    # Automatically creates an empty list on first access!
    grouped_orders[order['customer_id']].append(order)

4. Hàng đợi ưu tiên và Lựa chọn Top-K: heapq

Nếu dịch vụ backend của bạn cần liên tục lên lịch các công việc theo mức độ ưu tiên hoặc tìm 10 mục có giá trị cao nhất trong một luồng 1.000.000 sự kiện, việc sắp xếp toàn bộ danh sách bằng list.sort() là O(n \log n)—một sự lãng phí lớn chu kỳ CPU.

Mô-đun heapq triển khai min-heap nhị phân trực tiếp trên các danh sách Python tiêu chuẩn:

import heapq

class TaskScheduler:
    def __init__(self):
        self._heap = []

    def push_task(self, priority: int, task_name: str):
        # Min-heap orders lowest priority number first (e.g. 1 = critical)
        heapq.heappush(self._heap, (priority, task_name))

    def pop_next_task(self) -> str:
        priority, task_name = heapq.heappop(self._heap)
        return task_name

# Finding Top-K items in O(n log k) instead of O(n log n)
large_dataset = [15, 3, 99, 42, 8, 104, 2, 77, 63]
top_3_largest = heapq.nlargest(3, large_dataset)
print(top_3_largest) # [104, 99, 77]

Advertisement

5. Giảm bộ nhớ 60%: Dataclass với __slots__

Trong Python, mỗi thể hiện lớp tiêu chuẩn duy trì một từ điển ẩn (__dict__) để cho phép tạo thuộc tính động tùy ý trong thời gian chạy. Khi khởi tạo 1.000.000 bản ghi từ cơ sở dữ liệu hoặc xuất CSV, chi phí bộ nhớ của __dict__ chiếm ưu thế trong dấu chân bộ nhớ của bạn.

Bằng cách khai báo slots=True trên các dataclass Python hiện đại, bạn hướng dẫn thời gian chạy CPython cấp phát một mảng bộ nhớ có kích thước cố định thay thế:

from dataclasses import dataclass
import sys

# Standard dataclass (Allocates __dict__ for every instance)
@dataclass
class StandardRecord:
    id: int
    name: str
    amount: float

# Slotted dataclass (Zero __dict__ overhead)
@dataclass(slots=True)
class OptimizedRecord:
    id: int
    name: str
    amount: float

std_obj = StandardRecord(1, "Account_A", 150.0)
opt_obj = OptimizedRecord(1, "Account_A", 150.0)

# Memory consumption comparison
print("Standard object size:", sys.getsizeof(std_obj) + sys.getsizeof(std_obj.__dict__))
# ~152 bytes per instance
print("Slotted object size: ", sys.getsizeof(opt_obj))
# ~56 bytes per instance (63% memory reduction!)

Ma trận so sánh hiệu suất

Bảng dưới đây tóm tắt độ phức tạp hoạt động và các điểm chuẩn hiệu suất trên 100.000 hoạt động trong Python 3.12:

Thao tácCấu trúc tiêu chuẩnThay thế nâng caoTăng tốc / Lợi thế
FIFO Pop (Hàng đợi)list.pop(0): 1.840 ms (O(n))deque.popleft(): 6.2 ms (O(1))Nhanh hơn 296 lần
Đếm tần suấtVòng lặp dict thủ công: 48 mscollections.Counter: 12 msNhanh hơn 4 lần
Top 10 trong 1 triệu mụcsorted(items)[:10]: 142 msheapq.nsmallest(10): 18 msNhanh hơn 7.8 lần
1 triệu đối tượng trong RAMDataclass tiêu chuẩn: ~180 MBDataclass slots=True: ~68 MBTiết kiệm 62% bộ nhớ

Các câu hỏi thường gặp

deque có an toàn cho luồng trong Python không?

Có. Cả các thao tác append(), appendleft(), pop() và popleft() trong collections.deque đều là nguyên tử và an toàn cho luồng trong CPython nhờ Khóa Trình thông dịch Toàn cục (GIL) và khóa mutex nội bộ. Bạn có thể an toàn truyền một deque giữa các luồng người tiêu dùng và nhà sản xuất mà không cần các nguyên thủy khóa bên ngoài.

Khi nào tôi nên ưu tiên Counter hơn một GROUP BY SQL?

Nếu dữ liệu của bạn đã nằm trong PostgreSQL hoặc ClickHouse, hãy để cơ sở dữ liệu thực hiện tổng hợp. Tuy nhiên, khi truyền nhật ký, xử lý văn bản không có cấu trúc hoặc tính toán tần suất trong quá trình chuyển đổi tải trọng API, Counter thực thi trong bộ nhớ với độ trễ gần như bằng không.

slots=True có ngăn thêm các thuộc tính mới một cách linh hoạt không?

Có. Khai báo slots=True khóa các thuộc tính đối tượng với những thuộc tính được định nghĩa rõ ràng trong lược đồ lớp. Mọi nỗ lực gán obj.new_field = "value" một cách linh hoạt sẽ gây ra AttributeError. Hạn chế này là một lựa chọn thiết kế có chủ ý nhằm đảm bảo hiệu quả bộ nhớ và phát hiện lỗi chính tả.


Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement