Generator trong Python

Trong bài viết này, Quantrimang sẽ cùng bạn tìm hiểu cách tạo Iterator bằng cách sử dụng Generator trong Python. Generator khác với iterator và các hàm thông thường như thế nào, tại sao ta nên sử dụng nó? Cùng tìm hiểu tất cả qua các nội dung sau.

Generator trong Python

Để xây dựng một iterator, ta cần phải theo dõi khá nhiều bước ví dụ như: triển khai class với phương thức __iter__()__next__(), theo dõi các tình trạng bên trong, StopIteration xảy ra khi không có giá trị nào được trả về...

Generator được dùng để giải quyết các vấn đề này. Generator là cách đơn giản để tạo ra iterator. 

Nói một cách đơn giản, generator là một hàm trả về một đối tượng (iterator) mà chúng ta có thể lặp lại (một giá trị tại một thời điểm). Chúng cũng tạo ra một đối tượng kiểu danh sách, nhưng bạn chỉ có thể duyệt qua các phần tử của generator một lần duy nhất vì generator không lưu dữ liệu trong bộ nhớ mà cứ mỗi lần lặp thì chúng sẽ tạo phần tử tiếp theo trong dãy và trả về phần tử đó.

Làm thể nào để tạo Generator trong Python

Để tạo generator trong Python, bạn sử dụng từ khóa def giống như khi định nghĩa một hàm. Trong generator, dùng câu lệnh yield để trả về các phần tử thay vì câu lệnh return như bình thường. 

Nếu một hàm chứa ít nhất một yield (có thể có nhiều yield và thêm cả return) thì chắc chắn đây là một hàm generator. Trong trường hợp này, cả yieldreturn sẽ trả về các giá trị từ hàm.

Điều khác biệt ở đây là return sẽ chấm dứt hoàn toàn một hàm, còn yield sẽ chỉ tạm dừng các trạng thái bên trong hàm và sau đó vẫn có thể tiếp tục khi được gọi trong các lần sau.

Ví dụ, khi bạn gọi phương thức __next__() lần thứ nhất, generator thực hiện các công việc tính toán giá trị rồi gặp từ khóa yield nào thì nó sẽ trả về các phần tử tại ví trí đó, khi bạn gọi phương thức __next__() lần thứ hai thì generator không bắt đầu chạy tại vị trí đầu tiên mà bắt đầu ngay phía sau từ khóa yield thứ nhất. Cứ như thế generator tạo ra các phần tử trong dãy, cho đến khi không còn gặp từ khóa yield nào nữa thì giải phóng exception StopIteration.

Sự khác biệt của hàm generator và hàm thông thường

Đây là một số khác biệt giữa hàm generator và hàm thông thường:

  • Hàm generator chứa một hoặc nhiều câu lệnh yield.
  • Khi được gọi, generator trả về một đối tượng (iterator) nhưng không bắt đầu thực thi ngay lập tức.
  • Các phương thức như __iter __() và __next__() được triển khai tự động. Vì vậy, chúng ta có thể lặp qua các mục bằng cách sử dụng next().
  • Yield sẽ tạm dừng hàm, các biến cục bộ và trạng thái của chúng được ghi nhớ giữa các lệnh gọi liên tiếp. Mỗi lần lệnh yield được chạy, nó sẽ sinh ra một giá trị mới. 
  • Cuối cùng, khi hàm kết thúc, StopIteration sẽ xảy ra nếu tiếp tục gọi hàm.

Dưới đây là một ví dụ để minh họa tất cả các điểm đã nêu ở trên. Chúng ta có một hàm generator có tên my_gen() với một số câu lệnh yield.

# Hàm generator đơn giản
 # Viet boi Quantrimang.com
 def my_gen():
     n = 1
     print('Doan text nay duoc in dau tien')
     # Hàm Generator chứa các câu lệnh yield 
     yield n
 
     n += 1
     print('Doan text nay duoc in thu hai')
     yield n
 
     n += 1
     print('Doan text nay duoc in cuoi cung')
     yield n

Chạy chúng trong Python shell để xem output:

>>> # Trả về một đối tượng nhưng không bắt đầu thực thi ngay lập tức.
 >>> a = my_gen()
 >>> # Chúng ta có thể lặp qua các mục bằng cách sử dụng next().
 >>> next(a)
 Doan text nay duoc in dau tien
 1
 >>> # Yield sẽ tạm dừng hàm, quyền điều khiển chuyển đến người gọi
 >>> # Các biến cục bộ và trạng thái của chúng được ghi nhớ giữa các 
 lệnh gọi liên tiếp.
 >>> next(a)
 Doan text nay duoc in thu hai
 2
 >>> next(a)
 Doan text nay duoc in cuoi cung
 3
 >>> # Cuối cùng, khi hàm kết thúc, StopIteration sẽ xảy ra nếu tiếp 
 tục gọi hàm.
 >>> next(a)
 Traceback (most recent call last):
 ...
 StopIteration
 >>> next(a)
 Traceback (most recent call last):
 ...
 StopIteration

Có thể thấy trong ví dụ trên là giá trị của biến n được ghi nhớ giữa các lần gọi, không giống như các hàm bình thường kết thúc ngay sau mỗi lần gọi.

Khi bạn gọi phương thức next() lần thứ nhất, generator thực hiện các công việc tính toán giá trị rồi trả về phần tử tại ví trí đó, khi gọi phương thức next() lần thứ hai thì generator không bắt đầu chạy tại vị trí đầu tiên mà bắt đầu ngay phía sau từ khóa yield thứ nhất. Cứ như thế generator tạo ra các phần tử trong dãy, cho đến khi không còn gặp từ khóa yield nào nữa thì giải phóng exception StopIteration.

Để khởi động lại quá trình, tạo một đối tượng generator khác bằng cách sử dụng đối tượng như a = my_gen().

Lưu ý: Có thể sử dụng generator trực tiếp cho các vòng lặp for.

Vòng lặp for lấy một iterator và lặp lại nó bằng hàm next(), tự động kết thúc khi StopIteration xảy ra. 

# Hàm generator đơn giản
 def my_gen():
     n = 1
     print('Doan text nay duoc in dau tien')
     # Hàm Generator chứa câu lệnh yield 
     yield n
 
     n += 1
     print('Doan text nay duoc in thu hai')
     yield n
 
     n += 1
     print('Doan text nay duoc in cuoi cung')
     yield n
 
 # Sử dụng vòng lặp for 
 for item in my_gen():
     print(item)    

Chạy chương trình, kết quả trả về là:

Doan text nay duoc in dau tien
 1
 Doan text nay duoc in thu hai
 2
 Doan text nay duoc in cuoi cung
 3

Generator với các vòng lặp trong Python

Ví dụ về generator đảo ngược chuỗi.

def rev_str(my_str):
     length = len(my_str)
     for i in range(length - 1,-1,-1):
         yield my_str[i]
 
 # Vòng lặp for đảo ngược chuỗi
 # Viết bởi Quantrimang.com
 # Output:
 # o
 # l
 # l
 # e
 # h
 for char in rev_str("hello"):
      print(char)

Ví dụ này sử dụng hàm range() để lấy chỉ mục theo thứ tự ngược lại trong vòng lặp for.

Biểu thức generator

Generator có thể dễ dàng được tạo ra khi sử dụng biểu thức generator.

Giống như Lambda tạo một hàm vô danh trong Python, generator cũng tạo một biểu thức generator vô danh. Cú pháp tương tự như cú pháp của list comprehension, nhưng dấu ngoặc vuông được thay thế bằng dấu ngoặc tròn.

List comprehension thì trả về một list, còn biểu thức generator trả về một generator tại một thời điểm khi được yêu cầu. Vì lý do này, biểu thức generator sử dụng ít bộ nhớ hơn, đem lại hiệu quả hiệu suất hơn so với list comprehension tương đương.

# Khởi tạo danh sách
 my_list = [1, 3, 6, 10]
 
 # bình phương mỗi phần tử bằng cách sử dụng list comprehension
 # Output: [1, 9, 36, 100]
 [x**2 for x in my_list]
 
 # kết quả tương tự khi sử dụng biểu thức generator
 # Output: <generator object <genexpr> at 0x0000000002EBDAF8>
 (x**2 for x in my_list)

Có thể thấy ở ví dụ trên, biểu thức generator không tạo ra kết quả cần thiết ngay lập tức mà trả về đối tượng generator, cứ mỗi lần lặp thì chúng sẽ tạo phần tử tiếp theo trong dãy và trả về phần tử đó.

# Khởi tạo danh sách
 my_list = [1, 3, 6, 10]
 
 a = (x**2 for x in my_list)
 # Output: 1
 print(next(a))
 
 # Output: 9
 print(next(a))
 
 # Output: 36
 print(next(a))
 
 # Output: 100
 print(next(a))
 
 # Output: StopIteration
 next(a)

Biểu thức generator sử dụng bên trong các hàm thì có thể bỏ qua các dấu ngoặc tròn.

>>> sum(x**2 for x in my_list)
 146
 >>> max(x**2 for x in my_list)
 100

Tại sao nên sử dụng generator trong Python?

Việc sử dụng generator sẽ đem lại nhiều tác dụng hấp dẫn.

1. Đơn giản hóa code, dễ triển khai

Generator có thể giúp code được triển khai rõ ràng và ngắn gọn hơn so với lớp iterator tương tự. Để minh họa cho việc này, chúng ta sẽ lấy một ví dụ cụ thể.

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

Đoạn code này khá dài. Bây giờ hãy thử sử dụng hàm generator.

def PowTwoGen(max = 0):
     n = 0
     while n < max:
         yield 2 ** n
         n += 1

Ở đây, generator thực hiện ngắn gọn và gọn gàng hơn nhiều.

2. Sử dụng ít bộ nhớ

Một hàm thông thường khi trả về list sẽ lưu toàn bộ list trong bộ nhớ. Trong phần lớn các trường hợp, điều đó là không hay khi phải sử dụng đến dung lượng bộ nhớ lớn đến vậy.

Generator sẽ sử dụng ít bộ nhớ hơn vì chúng chỉ thực sự tạo kết quả khi được gọi tới, sinh ra một phần tử tại một thời điểm, đem lại hiệu quả nếu chúng ta không có nhu cầu duyệt nó quá nhiều lần.

3.Tạo ra các list vô hạn

Generator là phương tiện tuyệt vời để tạo ra một luồng dữ liệu vô hạn. Các luồng vô hạn này không cần lưu trữ toàn bộ trong bộ nhớ vì generator chỉ tạo ra một phần tử tại một thời điểm, nên nó có thể biểu thị luồng dữ liệu vô hạn.

Ví dụ sau có thể tạo ra tất cả các số chẵn.

def all_even():
     n = 0
     while True:
         yield n
         n += 2

Nói chung, việc lựa chọn sử dụng generator phụ thuộc nhiều vào thực tế yêu cầu của công việc. Hãy suy nghĩ và lựa chọn cẩn thận để có được lựa chọn tốt nhất cho mình.

Bài trước: Đối tượng Iterator trong Python

Bài tiếp: Cách sử dụng Closure trong Python

Thứ Tư, 17/07/2019 16:43
4,68 👨 180