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

Python Generators: Tăng hiệu năng và đơn giản hóa mã

Tìm hiểu về generator trong Python để tối ưu bộ nhớ và đơn giản hóa quy trình. Xây dựng pipeline dữ liệu hiệu quả trong thực tế và tăng hiệu năng cho mã của bạn.
· 10 phút đọc

Khám phá cùng AI

Hãy tưởng tượng bạn đang làm một dự án khoa học dữ liệu, và nhiệm vụ là xử lý một tập dữ liệu lớn đến mức chỉ cần nạp vào bộ nhớ cũng khiến máy bị treo. Hoặc bạn phải làm việc với một dãy vô hạn, như luồng dữ liệu thời gian thực, nơi bạn không thể lưu trữ mọi thứ cùng lúc. Đây là những thách thức dễ khiến các nhà khoa học dữ liệu phải tìm đến bình cà phê—và đôi khi là nút khởi động lại.  

Trong bài viết này, chúng ta sẽ tìm hiểu về generator trong Python và cách bạn có thể dùng chúng để đơn giản hóa mã. Ý tưởng này cần luyện tập một chút, nên nếu bạn mới học Python và cảm thấy hơi lạc hướng khi đọc, hãy thử khóa học Introduction to Python để xây nền tảng vững chắc. 

Generator trong Python là gì?

Về cốt lõi, generator trong Python là một kiểu hàm đặc biệt hoặc thậm chí là một biểu thức gọn, tạo ra một dãy giá trị theo cách “lười biếng”. Hãy hình dung generator như một băng chuyền trong nhà máy: thay vì chất đống mọi sản phẩm vào một chỗ rồi hết chỗ chứa, bạn xử lý từng món khi nó đi đến. Điều này giúp generator tiết kiệm bộ nhớ và là phần mở rộng tự nhiên của giao thức iterator trong Python, vốn là nền tảng cho nhiều công cụ dựng sẵn như vòng lặp for và comprehension.  

Điều kỳ diệu phía sau generator nằm ở từ khóa yield. Khác với return, vốn trả về một giá trị rồi thoát hàm, yield tạo ra một giá trị, tạm dừng việc thực thi của hàm và lưu lại trạng thái. Khi gọi generator lần nữa, nó tiếp tục từ đúng nơi đã dừng. 

Ví dụ, hãy tưởng tượng bạn đang đọc một tệp log khổng lồ từng dòng. Một generator có thể xử lý từng dòng khi đọc mà không cần nạp cả tệp vào bộ nhớ. Cơ chế “đánh giá lười biếng” này tạo nên sự khác biệt của generator so với hàm truyền thống và khiến chúng trở thành công cụ ưa thích cho các tác vụ nhạy về hiệu năng.  

Ví dụ cơ bản về Python Generator

Hãy thực hành một chút để nắm ý tưởng. Đây là một hàm generator tạo ra n số nguyên đầu tiên.  

def generate_integers(n):
    for i in range(n):
        yield i  # Pauses here and returns i

# Using the generator
for num in generate_integers(5):
    print(num)
0
1
2
3
4

Tôi đã tạo một hình minh họa để giúp bạn thấy chuyện gì diễn ra bên dưới:  

Lưu đồ hàm generator trong Python

Cú pháp và mẫu sử dụng Python Generator  

Generator có thể được triển khai theo nhiều cách. Tuy vậy, có hai cách chính: hàm generator và biểu thức generator.

Hàm generator  

Hàm generator được định nghĩa như hàm thông thường nhưng dùng từ khóa yield thay cho return. Khi được gọi, nó trả về một đối tượng generator có thể lặp qua.  

def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

# Using the generator
counter = count_up_to(5)
for num in counter:
    print(num)
1
2
3
4
5

Từ ví dụ trên, ta thấy khi gọi hàm count_up_to, nó trả về một đối tượng generator. Mỗi lần vòng lặp for yêu cầu một giá trị, hàm chạy cho đến khi gặp yield, tạo ra giá trị hiện tại của count và bảo toàn trạng thái giữa các lần lặp để tiếp tục đúng vị trí trước đó.

Biểu thức generator  

Biểu thức generator là cách gọn để tạo generator. Chúng giống list comprehension nhưng dùng ngoặc tròn thay vì ngoặc vuông.

# List comprehension (eager evaluation)
squares_list = [x**2 for x in range(5)]  # [0, 1, 4, 9, 16]

# Generator expression (lazy evaluation)
squares_gen = (x**2 for x in range(5))

# Using the generator
for square in squares_gen:
    print(square)
0
1
4
9
16

Vậy khác biệt giữa list comprehension và biểu thức generator là gì? List comprehension tạo toàn bộ danh sách trong bộ nhớ, trong khi biểu thức generator tạo ra giá trị từng lần, tiết kiệm bộ nhớ. Nếu bạn chưa quen với list comprehension, bạn có thể đọc thêm trong Hướng dẫn Python List Comprehension của chúng tôi.

Generator so với iterator

Iterator truyền thống trong Python yêu cầu các lớp với phương thức __iter__() và __next__() tường minh, kéo theo nhiều mã lặp khuôn và quản lý trạng thái thủ công, trong khi hàm generator đơn giản hóa quy trình bằng cách tự động bảo toàn trạng thái và loại bỏ nhu cầu các phương thức này—như được minh họa bởi một hàm đơn giản trả về bình phương của từng số đến n.

Vì sao chúng ta dùng Python Generators  

Khi giải thích generator trong Python là gì, tôi cũng đã gợi mở phần nào lý do vì sao chúng được dùng. Ở phần này, tôi muốn đi chi tiết hơn một chút. Bởi vì generator không chỉ là một tính năng “hào nhoáng” của Python, mà thực sự giải quyết vấn đề thực tế.

Hiệu quả bộ nhớ  

Khác với list hoặc mảng, vốn lưu tất cả phần tử trong bộ nhớ cùng lúc, generator tạo giá trị ngay khi cần, nên chỉ giữ một phần tử tại một thời điểm.  

Ví dụ, hãy xem sự khác biệt giữa range() và xrange() trong Python 2:  

  •  range() tạo một list trong bộ nhớ, có thể gây vấn đề với phạm vi lớn.

  •  xrange() hoạt động như một generator, tạo giá trị theo cách lười biếng.

Vì hành vi của xrange() hữu ích hơn, hiện nay trong Python 3, range() cũng hoạt động như một generator, tránh chi phí bộ nhớ khi lưu trữ toàn bộ giá trị cùng lúc.  

Để minh họa, hãy so sánh mức dùng bộ nhớ khi tạo một dãy 10 triệu số:  

import sys

# Using a list
numbers_list = [x for x in range(10_000_000)]
print(f"Memory used by list: {sys.getsizeof(numbers_list) / 1_000_000:.2f} MB")

# Using a generator
numbers_gen = (x for x in range(10_000_000))
print(f"Memory used by generator: {sys.getsizeof(numbers_gen)} bytes")
Memory used by list: 89.48 MB
Memory used by the generator: 112 bytes

Như bạn thấy, generator gần như không dùng bộ nhớ so với list, và sự khác biệt này rất đáng kể.  

Cải thiện hiệu năng  

Nhờ cơ chế đánh giá lười biếng, giá trị chỉ được tính khi cần. Điều này nghĩa là bạn có thể bắt đầu xử lý dữ liệu ngay mà không cần chờ tạo xong cả dãy.  

Chẳng hạn, tưởng tượng tính tổng bình phương của 1 triệu số đầu tiên:

# Using a list (eager evaluation)
sum_of_squares_list = sum([x**2 for x in range(1_000_000)])

# Using a generator (lazy evaluation)
sum_of_squares_gen = sum(x**2 for x in range(1_000_000))

Cả hai cách cho kết quả như nhau, nhưng phiên bản dùng generator tránh tạo một list khổng lồ, nên chúng ta có kết quả nhanh hơn. 

Đơn giản và dễ đọc  

Generator đơn giản hóa việc triển khai iterator bằng cách loại bỏ mã khuôn. So sánh iterator dựa trên lớp với hàm generator:  

Đây là iterator dựa trên lớp:

class SquaresIterator:
    def __init__(self, n):
        self.n = n
        self.current = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.current >= self.n:
            raise StopIteration
        result = self.current ** 2
        self.current += 1
        return result

# Usage
squares = SquaresIterator(5)
for square in squares:
    print(square)

Đây là hàm generator:

def squares_generator(n):
    for i in range(n):
        yield i ** 2

# Usage
squares = squares_generator(5)
for square in squares:
    print(square)

Phiên bản generator ngắn hơn, dễ đọc hơn và không cần mã khuôn. Đây là ví dụ hoàn hảo cho triết lý của Python: đơn giản là tốt hơn.  

Xử lý dãy vô hạn  

Cuối cùng, tôi muốn nói rằng generator đặc biệt phù hợp để biểu diễn các dãy vô hạn, điều mà list không thể làm được. Ví dụ, hãy xem dãy Fibonacci:  

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Usage
fib = fibonacci()
for _ in range(10):
    print(next(fib))
0
1
1
2
3
5
8
13
21
34

Generator này có thể tạo số Fibonacci vô thời hạn mà không cạn bộ nhớ. Ví dụ khác gồm xử lý luồng dữ liệu trực tiếp hoặc làm việc với dữ liệu chuỗi thời gian.  

Các khái niệm nâng cao về Python Generator  

Giờ hãy xem một số ý tưởng khó hơn. Phần này, chúng ta sẽ khám phá cách kết hợp generator và dùng các phương thức đặc biệt như .send(), .throw() và .close().  

Xâu chuỗi các generator

Generator có thể được kết hợp. Bạn có thể biến đổi, lọc và xử lý dữ liệu theo kiểu mô-đun bằng cách xâu chuỗi các generator. 

Giả sử bạn có một dãy số vô hạn và muốn bình phương mỗi số rồi lọc bỏ kết quả lẻ:

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1
def square_numbers(sequence):
    for num in sequence:
        yield num ** 2
def filter_evens(sequence):
    for num in sequence:
        if num % 2 == 0:
            yield num

# Compose the generators
numbers = infinite_sequence()
squared = square_numbers(numbers)
evens = filter_evens(squared)

# Print the first 10 even squares
for _ in range(10):
    print(next(evens))
0
4
16
36
64
100
144
196
256
324

Quy trình gồm hàm infinite_sequence tạo số vô hạn, square_numbers trả về bình phương mỗi số, rồi filter_evens lọc bỏ số lẻ để chỉ tạo ra bình phương chẵn. Chương trình nghề nghiệp Associate Python Developer của chúng tôi đi sâu vào kiểu nội dung này, để bạn thấy cách xây dựng và gỡ lỗi pipeline phức tạp bằng generator, cũng như iterator và list comprehension. 

Các phương thức đặc biệt của generator  

Generator có các phương thức nâng cao cho phép giao tiếp hai chiều và kết thúc có kiểm soát.

send()

Phương thức .send() cho phép bạn truyền giá trị ngược vào generator, biến nó thành coroutine. Điều này hữu ích để tạo generator tương tác hoặc có trạng thái.  

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is not None:
            total += value

# Using the generator
acc = accumulator()
next(acc)  # Start the generator
print(acc.send(10))  # Output: 10
print(acc.send(5))   # Output: 15
print(acc.send(20))  # Output: 35

Cách hoạt động như sau: 

  • Khởi động generator bằng next(acc).  

  • Mỗi lần gọi .send(value) sẽ truyền một giá trị vào generator, được gán cho value tại biểu thức yield.  

  • Generator cập nhật trạng thái (total) và trả về kết quả mới.

throw()

Phương thức .throw() cho phép bạn phát sinh ngoại lệ bên trong generator, hữu ích cho xử lý lỗi hoặc báo hiệu điều kiện cụ thể.  

def resilient_generator():
    try:
        for i in range(5):
            yield i
    except ValueError:
        yield "Error occurred!"

# Using the generator
gen = resilient_generator()
print(next(gen))  # Output: 0
print(next(gen))  # Output: 1
print(gen.throw(ValueError))  # Output: "Error occurred!"

Cách hoạt động của phương thức này: 

  • Generator chạy bình thường cho đến khi gọi .throw().  

  •  Ngoại lệ được phát sinh bên trong generator và có thể được xử lý bằng khối try-except.

close()

Phương thức .close() dừng một generator bằng cách phát sinh ngoại lệ GeneratorExit. Điều này hữu ích để giải phóng tài nguyên hoặc dừng các generator vô hạn.  

def infinite_counter():
    count = 0
    try:
        while True:
            yield count
            count += 1
    except GeneratorExit:
        print("Generator closed!")

# Using the generator
counter = infinite_counter()

print(next(counter))  # Output: 0
print(next(counter))  # Output: 1
counter.close()       # Output: "Generator closed!"

Và đây là cách nó hoạt động: 

  • Generator chạy cho đến khi .close() được gọi.  

  •  Ngoại lệ GeneratorExit được phát sinh, cho phép generator dọn dẹp hoặc ghi log trước khi kết thúc.

Ứng dụng thực tế trong khoa học dữ liệu  

Hy vọng bạn đang dần thấy generator hữu ích. Ở phần này, tôi sẽ làm nổi bật một số tình huống sử dụng để bạn hình dung cách chúng hoạt động trong công việc hằng ngày. 

Xử lý tập dữ liệu lớn  

Một trong những thách thức phổ biến trong khoa học dữ liệu là làm việc với các tập dữ liệu quá lớn để nạp vào bộ nhớ. Generator cung cấp cách xử lý dữ liệu như vậy theo từng dòng.  

Giả sử bạn có một tệp CSV 10 GB chứa dữ liệu bán hàng và cần lọc các bản ghi cho một khu vực cụ thể. Đây là cách dùng pipeline generator để làm điều đó:  

import csv
def read_large_csv(file_path):
    """ Generator to read a large CSV file line by line."""
    with open(file_path, mode="r") as file:
        reader = csv.DictReader(file)
        for row in reader:
            yield row

def filter_by_region(data, region):
    """ Generator to filter rows by a specific region."""
    for row in data:
        if row["Region"] == region:
            yield row

# Generator pipeline
file_path = "sales_data.csv"
region = "North America"
data = read_large_csv(file_path)
filtered_data = filter_by_region(data, region)

# Process the filtered data
for record in filtered_data:
    print(record)

Diễn giải như sau: 

  1. read_large_csv đọc tệp theo từng dòng, trả về mỗi dòng dưới dạng từ điển.  

  2. filter_by_region lọc các dòng theo khu vực chỉ định.  

  3. Pipeline xử lý dữ liệu theo từng phần, tránh quá tải bộ nhớ.

Cách tiếp cận này hữu ích cho các quy trình ETL/ELT, nơi dữ liệu cần được làm sạch và chuyển đổi trước khi phân tích. Bạn sẽ thấy điều này trong khóa học ETL and ELT in Python của chúng tôi.

Streaming và pipeline  

Đôi khi dữ liệu đến như một luồng liên tục. Hãy nghĩ đến dữ liệu cảm biến, luồng trực tiếp, hoặc mạng xã hội.

Giả sử bạn làm việc với thiết bị IoT tạo ra số đo nhiệt độ mỗi giây. Bạn muốn tính nhiệt độ trung bình trên một cửa sổ trượt gồm 10 mẫu đọc:

def sensor_data_stream():
    """Simulate an infinite stream of sensor data."""
    import random
    while True:
        yield random.uniform(0, 100)  # Simulate sensor data

def sliding_window_average(stream, window_size):
    """ Calculate the average over a sliding window of readings."""
    window = []
    for value in stream:
        window.append(value)
        if len(window) > window_size:
            window.pop(0)
        if len(window) == window_size:
            yield sum(window) / window_size

# Generator pipeline
sensor_stream = sensor_data_stream()
averages = sliding_window_average(sensor_stream, window_size=10)

# Print the average every second
for avg in averages:
    print(f"Average temperature: {avg:.2f}")

Giải thích: 

  1. sensor_data_stream mô phỏng một luồng vô hạn các số đo cảm biến.  

  2. sliding_window_average duy trì một cửa sổ trượt gồm 10 mẫu gần nhất và trả về giá trị trung bình của chúng.  

  3. Pipeline xử lý dữ liệu theo thời gian thực, lý tưởng cho giám sát và phân tích.

Các trường hợp sử dụng bổ sung

Generator cũng được dùng khi kích thước dữ liệu không dự đoán được hoặc dữ liệu cứ tiếp tục đến/ở dạng vô hạn.  

Web scraping  

Khi crawl website, bạn thường không biết có bao nhiêu trang hoặc mục cần xử lý. Generator cho phép xử lý sự bất định này một cách linh hoạt:

def scrape_website(url):
    """ Generator to scrape a website page by page."""
    while url:
        # Simulate fetching and parsing a page
        print(f"Scraping {url}")
        data = f"Data from {url}"
        yield data
        url = get_next_page(url)  # Hypothetical function to get the next page

# Usage
scraper = scrape_website("https://example.com/page1")
for data in scraper:
    print(data)

Bài toán mô phỏng

Trong các mô phỏng, như phương pháp Monte Carlo hoặc phát triển game, generator có thể biểu diễn các dãy vô hạn hoặc động:

def monte_carlo_simulation():
    """ Generator to simulate random events for Monte Carlo analysis."""
    import random
    while True:
        yield random.random()

# Usage
simulation = monte_carlo_simulation()
for _ in range(10):
    print(next(simulation))

Đo lường bộ nhớ và tốc độ

Nhờ cách hoạt động của mình, generator vượt trội trong các kịch bản cần tiết kiệm bộ nhớ, nhưng (có thể bạn sẽ ngạc nhiên) không phải lúc nào cũng nhanh nhất. Hãy so sánh generator với list để hiểu thỏa hiệp giữa chúng.  

Trước đó, chúng ta đã cho thấy generator tốt hơn list về bộ nhớ, khi so sánh mức dùng bộ nhớ cho dãy 10 triệu số. Giờ hãy làm một bài so sánh khác, về tốc độ:

import time

# List comprehension
start_time = time.time()
sum([x**2 for x in range(1_000_000)])
print(f"List comprehension time: {time.time() - start_time:.4f} seconds")

# Generator expression
start_time = time.time()
sum(x**2 for x in range(1_000_000))
print(f"Generator expression time: {time.time() - start_time:.4f} seconds")
List comprehension time: 0.1234 seconds
Generator expression time: 0.1456 seconds

Dù generator tiết kiệm bộ nhớ, trong trường hợp này, nó thực sự chậm hơn list. Lý do là với tập dữ liệu nhỏ, có chi phí cho việc tạm dừng và tiếp tục thực thi.  

Sự khác biệt hiệu năng là không đáng kể với tập dữ liệu nhỏ, nhưng với tập lớn, lợi ích về bộ nhớ của generator thường vượt trội so với mức phạt tốc độ nhỏ.  

Những vấn đề thường gặp

Cuối cùng, hãy xem một số lỗi hoặc vấn đề phổ biến:

Generator có thể bị “cạn” 

Một khi generator đã cạn, bạn không thể dùng lại. Bạn sẽ cần tạo lại nếu muốn lặp lần nữa.

gen = (x for x in range(5))
print(list(gen))  # Output: [0, 1, 2, 3, 4]
print(list(gen))  # Output: [] (the generator is exhausted)

Đánh giá lười biếng có thể gây khó 

Vì generator tạo giá trị theo yêu cầu, lỗi hoặc tác dụng phụ có thể chỉ xuất hiện khi bạn lặp qua generator.  

Có thể lạm dụng generator 

Với tập dữ liệu nhỏ hoặc tác vụ đơn giản, chi phí dùng generator có thể không đáng so với lợi ích bộ nhớ. Hãy xem ví dụ nơi tôi vật hóa dữ liệu để lặp nhiều lần.

# Generator expression
gen = (x**2 for x in range(10))

# Materialize into a list
squares = list(gen)

# Reuse the list
print(sum(squares))  # Output: 285
print(max(squares))  # Output: 81

Cách chọn khi nào dùng generator

Tóm lại, tôi sẽ đưa ra một vài quy tắc tổng quát về thời điểm dùng generator. Hãy dùng cho:

  • Tập dữ liệu lớn: Dùng generator khi làm việc với tập dữ liệu quá lớn để nạp vào bộ nhớ.  
  • Dãy vô hạn: Dùng generator để biểu diễn dãy vô hạn, như luồng dữ liệu trực tiếp hoặc mô phỏng.
  • Pipeline: Dùng generator để xây dựng pipeline xử lý dữ liệu theo mô-đun, biến đổi và lọc dữ liệu một cách tuần tự.  

Khi nào nên vật hóa dữ liệu (chuyển thành list)  

  • Tập dữ liệu nhỏ: Không dùng generator nếu bộ nhớ không phải vấn đề và bạn cần truy cập nhanh mọi phần tử; thay vào đó dùng list.  
  • Lặp nhiều lần: Không dùng generator nếu bạn cần lặp lại cùng dữ liệu nhiều lần; thay vào đó, vật hóa thành list để tránh phải tạo lại generator.  

Kết luận và điểm chính  

Xuyên suốt bài viết, chúng ta đã khám phá cách generator giúp bạn giải quyết thách thức thực tế trong khoa học dữ liệu, từ xử lý tập dữ liệu lớn đến xây dựng pipeline dữ liệu theo thời gian thực. Hãy tiếp tục luyện tập. Cách tốt nhất để giỏi generator là dùng chúng trong công việc của bạn. Bắt đầu bằng cách thay list comprehension bằng một biểu thức generator hoặc tái cấu trúc một vòng lặp thành hàm generator. 

Khi đã nắm vững cơ bản, bạn có thể khám phá các chủ đề mới và nâng cao dựa trên khái niệm generator:  

  • Coroutine: Dùng .send() và .throw() để tạo generator có thể nhận và xử lý dữ liệu, cho phép giao tiếp hai chiều.  

  • Lập trình bất đồng bộ: Kết hợp generator với thư viện asyncio của Python để xây dựng ứng dụng hiệu quả, không chặn.  

  • Đồng thời: Tìm hiểu cách generator triển khai đa nhiệm hợp tác và đồng thời nhẹ.  

Tiếp tục học và trở thành chuyên gia. Hãy theo học lộ trình nghề nghiệp Python Developer hoặc lộ trình kỹ năng Python Programming ngay hôm nay. Nhấp vào liên kết bên dưới để bắt đầu.

Oluseye Jeremiah

Tác giả

Oluseye Jeremiah

Nhà viết nội dung về công nghệ, chuyên về AI, ML và khoa học dữ liệu, giúp làm rõ và phổ biến những ý tưởng phức tạp.

Câu hỏi thường gặp về Python Generators

Chính xác thì Python generator là gì?

Python generator là một kiểu hàm đặc biệt sử dụng từ khóa yield để trả về một iterator, tạo ra giá trị từng cái một và tiết kiệm bộ nhớ vì không lưu trữ toàn bộ dãy cùng lúc.

Khi nào tôi nên dùng generator thay vì list comprehension?

Generator lý tưởng để xử lý các dãy lớn hoặc vô hạn khi hiệu quả bộ nhớ là then chốt, trong khi list comprehension phù hợp khi bạn cần một danh sách đầy đủ để truy cập lặp lại hoặc đánh chỉ mục ngẫu nhiên.

Generator cải thiện hiệu năng như thế nào?

Bằng cách trả về từng giá trị một (đánh giá lười biếng), generator tính toán giá trị ngay khi cần, giúp giảm sử dụng bộ nhớ và tăng tốc xử lý so với việc xây dựng toàn bộ cấu trúc dữ liệu trong bộ nhớ.

Tôi có thể lặp qua một generator nhiều hơn một lần không?

Không, generator sẽ cạn sau khi lặp xong một lần. Để lặp lại, bạn cần tạo một instance generator mới.

Một số trường hợp sử dụng thực tế của generator là gì?

Generator hữu ích để xử lý tập dữ liệu lớn, stream dữ liệu thời gian thực, xây dựng pipeline hiệu quả và xử lý dãy vô hạn như trong các tác vụ thuật toán và mô phỏng.

Chủ đề

Liên quan