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__() và __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ả yield
và return
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ệnhyield
đượ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ạn nên đọc
-
Cách sử dụng List comprehension trong Python
-
Lập trình hướng đối tượng trong Python
-
Hàm setattr() trong Python
-
Regular Expression (RegEx) trong Python
-
Hàm vô danh, Lambda trong Python
-
Đối tượng Iterator trong Python
-
Hàm dict() trong Python
-
Hàm format() trong Python
-
Hàm trong Python là gì? Các hàm trong Python
Cũ vẫn chất
-
DDD là gì? Destroy Dick December là gì?
Hôm qua 6 -
Cách sửa lỗi 0x0000001E: KMODE EXCEPTION NOT HANDLED trên Windows
Hôm qua -
Cách bật, tắt chế độ tạm thời trên Instagram tự xóa tin nhắn
Hôm qua -
Cách gạch ngang chữ trong Word, v̶i̶ế̶t̶ ̶c̶h̶ữ̶ ̶g̶ạ̶c̶h̶ ̶n̶g̶a̶n̶g̶ trong Word và Excel
Hôm qua -
Hướng dẫn chơi game trên Telegram
Hôm qua -
Nên xem Attack on Titan theo thứ tự nào?
Hôm qua -
Cách cài tiếng Việt cho Minecraft
Hôm qua -
Hướng dẫn đổi ID Facebook, thay địa chỉ Facebook mới
Hôm qua -
Có nên hack spin Coin Master, hack spin Coin Master có mất tài khoản không?
Hôm qua -
Cách cài WARP 1.1.1.1 trên máy tính để vào web bị chặn
Hôm qua 38