Sửa lỗi MemoryError trong Python khi xử lý dữ liệu lớn

intermediate🐍 Python2026-03-21| Python 3.x, Linux / macOS / Windows, máy có RAM giới hạn

Error Message

MemoryError
#python#bộ nhớ#hiệu năng#dữ liệu lớn

Lỗi Gặp Phải

Bạn đang xử lý một file CSV lớn, tải dataset vào list, hoặc chạy tính toán nặng — rồi Python đột ngột dừng lại:

MemoryError

Đôi khi bạn nhận được traceback với thêm thông tin:

Traceback (most recent call last):
  File "process.py", line 12, in <module>
    data = [line for line in open('huge_file.csv').readlines()]
MemoryError

Python đã cố cấp phát bộ nhớ mà hệ thống không thể đáp ứng. RAM đã bị cạn kiệt.

Nguyên Nhân

Nguyên nhân phổ biến nhất: tải toàn bộ dữ liệu cùng một lúc. Lấy ví dụ một file CSV 4 GB. Đọc bằng readlines() và Python không chỉ dùng 4 GB — overhead của object Python nhân con số đó lên 3–5 lần, khiến bạn cần tới 12–20 GB RAM chỉ để lưu dữ liệu.

Các nguyên nhân khác:

  • Xây dựng list hoặc dict khổng lồ trong vòng lặp mà không giải phóng tham chiếu
  • Các phép tính NumPy tạo ra mảng trung gian kích thước lớn
  • Môi trường bị hạn chế: VPS 2 GB RAM, Docker container, CI runner
  • Python 32-bit đụng giới hạn cứng 2 GB không gian địa chỉ mỗi tiến trình

Cách Sửa 1: Đọc File Theo Từng Phần

Các file object của Python mặc định là lazy. Hãy tận dụng điều đó thay vì chống lại nó.

# Cách xấu: kéo toàn bộ file vào RAM
with open('huge_file.txt') as f:
    lines = f.readlines()  # MemoryError ở đây

# Cách tốt: từng dòng một, bộ nhớ O(1)
with open('huge_file.txt') as f:
    for line in f:
        process(line)

Với pandas, tham số chunksize cho bạn khả năng kiểm soát tương tự:

import pandas as pd

for chunk in pd.read_csv('huge_file.csv', chunksize=100_000):
    # chunk là DataFrame với 100k dòng — hoàn toàn quản lý được
    result = chunk.groupby('category')['value'].sum()
    save_partial_result(result)

Cách Sửa 2: Dùng Generator Thay Vì List

Xây dựng một list chỉ để duyệt qua một lần là lãng phí. Generator tính từng giá trị theo yêu cầu và hầu như không chiếm bộ nhớ.

# Cách xấu: 10 triệu số nguyên cùng lúc trong RAM
squares = [x**2 for x in range(10_000_000)]

# Cách tốt: generator expression, tính từng giá trị một
squares = (x**2 for x in range(10_000_000))

for val in squares:
    process(val)

Khi xử lý file, yield biến bất kỳ hàm nào thành generator:

def read_records(filepath):
    with open(filepath) as f:
        for line in f:
            yield parse(line)

for record in read_records('big.log'):
    process(record)

Cách Sửa 3: Giảm Bộ Nhớ Với NumPy dtypes

NumPy mặc định dùng float64 — 8 byte mỗi phần tử. Với 100 triệu phần tử, đó là 800 MB. Chuyển sang float32 và bạn giảm còn 400 MB. Dùng uint8 cho số nguyên 0–255 và chỉ còn 100 MB.

import numpy as np

# Mặc định: float64 = 8 byte/phần tử → ~800 MB cho 100M phần tử
arr = np.array(data)

# float32 = 4 byte/phần tử → ~400 MB
arr = np.array(data, dtype=np.float32)

# uint8 = 1 byte/phần tử → ~100 MB
arr = np.array(data, dtype=np.uint8)

Nguyên tắc tương tự áp dụng cho pandas — khai báo dtypes ngay từ đầu thay vì để pandas tự đoán:

df = pd.read_csv('data.csv', dtype={
    'user_id': 'int32',
    'score': 'float32',
    'category': 'category'  # chuỗi lặp lại → categorical tiết kiệm rất nhiều bộ nhớ
})

Cách Sửa 4: Dùng Memory-Mapped Files

Với các file nhị phân lớn hoặc mảng NumPy, memory mapping giao quyền kiểm soát phân trang cho hệ điều hành. Bạn có thể truy cập theo kiểu mảng mà không cần tải toàn bộ file — OS chỉ tải các trang bạn thực sự truy cập.

import numpy as np

# Không đọc toàn bộ file — chỉ ánh xạ nó
arr = np.load('large_array.npy', mmap_mode='r')

# Chỉ slice này được tải vào RAM
subset = arr[1000:2000]

Cách Sửa 5: Xử Lý Với Dask Cho DataFrame Ngoài RAM

Khi bạn cần các thao tác kiểu pandas trên dữ liệu không vừa trong RAM, Dask xử lý điều đó một cách tự nhiên. API gần như giống hệt, nhưng thực thi là lazy và theo chunk:

pip install dask[dataframe]
import dask.dataframe as dd

# Xây dựng đồ thị tính toán lazy — chưa tải gì cả
df = dd.read_csv('huge_file.csv')

# Cũng lazy
result = df.groupby('category')['value'].sum()

# .compute() kích hoạt thực thi thực sự, từng chunk một
print(result.compute())

Cách Sửa 6: Xóa Object Và Ép Garbage Collection

Làm việc với các object lớn theo tuần tự? Hãy xóa chúng tường minh khi dùng xong thay vì chờ GC của Python tự xử lý:

import gc

for batch in batches:
    result = process(batch)
    save(result)
    del result
    del batch
    gc.collect()  # ép thu gom nếu bộ nhớ đang căng

Hãy coi đây là biện pháp cuối cùng. Nếu bạn thường xuyên gặp MemoryError, việc tái cấu trúc xung quanh generator hoặc chunk sẽ mang lại lợi ích lâu dài hơn.

Cách Sửa 7: Thêm Swap Space (Linux)

Trên server mà bạn không thể tái cấu trúc ngay, swap space ngăn chương trình crash — với chi phí là tốc độ. Một file swap 4 GB trên SSD có thể làm chậm mọi thứ 10–20 lần so với RAM, nhưng vẫn hơn là tiến trình bị crash.

# Kiểm tra trạng thái hiện tại
free -h

# Tạo file swap 4GB
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Đây chỉ là giải pháp tạm thời. Nó không khắc phục nguyên nhân gốc rễ.

Xác Nhận Đã Sửa

Dùng memory_profiler để xác nhận các thay đổi của bạn thực sự giảm mức sử dụng bộ nhớ đỉnh:

pip install memory-profiler
from memory_profiler import profile

@profile
def my_function():
    # code của bạn ở đây
    pass

my_function()

Kết quả hiển thị mức sử dụng bộ nhớ theo từng dòng. Sau khi chuyển sang chunking hoặc generator, RAM đỉnh sẽ giảm từ hàng gigabyte xuống còn vài chục megabyte với hầu hết các tác vụ.

Theo dõi mức sử dụng trực tiếp trên terminal thứ hai trong khi script chạy:

watch -n 1 'free -h'

Phòng Ngừa

  • Profile trước khi mở rộng quy mô: Chạy memory_profiler trên mẫu nhỏ trước. Phát hiện một lần tăng bộ nhớ 10x ở 1k dòng rẻ hơn nhiều so với debug ở 10M dòng.
  • Mặc định dùng generator: Bất kỳ hàm nào tạo ra một chuỗi đều nên dùng yield trừ khi bạn có lý do cụ thể để hiện thực hóa toàn bộ list.
  • Khai báo dtypes tường minh trong pandas: Để pandas tự suy luận kiểu dữ liệu sẽ mặc định mọi thứ thành int64/float64. Với dataset 50 cột, điều đó thường tốn 2–4 lần bộ nhớ cần thiết.
  • Benchmark kích thước chunk: Quá nhỏ thì tăng overhead I/O; quá lớn thì đột biến bộ nhớ. Với hầu hết tác vụ, 50k–200k dòng mỗi chunk là điểm khởi đầu hợp lý — hãy tinh chỉnh từ đó.

Related Error Notes