Courses
Iterator là những đối tượng có thể được lặp qua. Chúng là một tính năng phổ biến của ngôn ngữ lập trình Python, thường được dùng trong vòng lặp và list comprehension. Bất kỳ đối tượng nào có thể tạo ra một iterator được gọi là iterable.
Có khá nhiều công việc khi xây dựng một iterator. Chẳng hạn, mỗi đối tượng iterator phải hiện thực cả hai phương thức __iter__() và __next__() . Ngoài các yêu cầu trên, phần hiện thực còn phải có cách theo dõi trạng thái nội tại của đối tượng và phát sinh ngoại lệ StopIteration khi không còn giá trị nào để trả về. Những quy tắc này được gọi là giao thức iterator.
Tự hiện thực một iterator là một quá trình dài hơi và không phải lúc nào cũng cần thiết. Một lựa chọn đơn giản hơn là dùng generator. Generator là một kiểu hàm đặc biệt sử dụng từ khóa yield để trả về một iterator có thể được lặp qua, từng giá trị một.
Khả năng phân biệt khi nào nên hiện thực một iterator và khi nào nên dùng một generator sẽ giúp bạn nâng cao kỹ năng lập trình Python. Trong phần còn lại của hướng dẫn này, chúng tôi sẽ nhấn mạnh những điểm khác biệt giữa hai đối tượng này để giúp bạn quyết định đâu là lựa chọn tốt nhất cho từng tình huống.
Thuật ngữ
|
Thuật ngữ |
Định nghĩa |
|
Iterable |
Một đối tượng Python có thể được lặp qua trong một vòng lặp. Ví dụ về iterable gồm có list, set, tuple, dictionary, string, v.v. |
|
Iterator |
Một iterator là đối tượng có thể được lặp qua. Do đó, iterator chứa một số lượng giá trị đếm được. |
|
Generator |
Một kiểu hàm đặc biệt không trả về một giá trị đơn lẻ: nó trả về một đối tượng iterator với một dãy giá trị. |
|
Lazy Evaluation |
Một chiến lược đánh giá trong đó một số đối tượng chỉ được tạo ra khi cần. Vì vậy, trong một số cộng đồng lập trình, lazy evaluation còn được gọi là “call-by-need”. |
|
Giao thức Iterator |
Tập hợp các quy tắc phải tuân theo để định nghĩa một iterator trong Python. |
|
next() |
Một hàm dựng sẵn dùng để trả về phần tử tiếp theo trong một iterator. |
|
iter() |
Một hàm dựng sẵn dùng để chuyển một iterable thành một iterator. |
|
yield() |
Một từ khóa trong Python tương tự như return, ngoại trừ việc |
Iterators & Iterables trong Python
Iterable là các đối tượng có khả năng trả về các phần tử của chúng lần lượt – chúng có thể được lặp qua. Các cấu trúc dữ liệu dựng sẵn phổ biến trong Python như list, tuple và set là các iterable. Những cấu trúc khác như string và dictionary cũng là iterable: một string có thể được lặp qua các ký tự của nó, và khóa của một dictionary có thể được lặp qua. Theo kinh nghiệm, hãy coi bất kỳ đối tượng nào có thể lặp trong vòng lặp for là một iterable.
Khám phá iterable trong Python qua ví dụ
Dựa trên các định nghĩa, ta có thể kết luận rằng mọi iterator đều là iterable. Tuy nhiên, không phải mọi iterable đều là iterator. Một iterable chỉ tạo ra một iterator khi nó được lặp.
Để minh họa, chúng ta sẽ khởi tạo một list, là một iterable, và tạo ra một iterator bằng cách gọi hàm dựng sẵn iter() trên list đó.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
Mặc dù bản thân list không phải là một iterator, việc gọi hàm iter() sẽ chuyển nó thành iterator và trả về đối tượng iterator.
Để cho thấy rằng không phải mọi iterable đều là iterator, chúng ta sẽ khởi tạo cùng một đối tượng list và thử gọi hàm next(), vốn được dùng để trả về phần tử tiếp theo trong một iterator.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
Trong đoạn mã trên, bạn có thể thấy rằng thử gọi hàm next() trên list đã phát sinh TypeError – tìm hiểu thêm về Xử lý ngoại lệ và lỗi trong Python. Hành vi này xảy ra đơn giản vì list là một iterable chứ không phải một iterator.
Khám phá iterator trong Python qua ví dụ
Vì vậy, nếu mục tiêu là lặp qua một list, thì trước hết phải tạo ra một đối tượng iterator. Chỉ khi đó chúng ta mới có thể điều khiển việc lặp qua các giá trị của list.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python sẽ tự động tạo một đối tượng iterator bất cứ khi nào bạn cố gắng lặp qua một đối tượng iterable.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
Khi ngoại lệ StopIteration được bắt, vòng lặp sẽ kết thúc.
Các giá trị lấy từ một iterator chỉ có thể được truy xuất theo thứ tự từ trái sang phải. Python không có hàm previous() để cho phép lập trình viên di chuyển lùi trong một iterator.
Bản chất lười (lazy) của iterator
Ta có thể định nghĩa nhiều iterator dựa trên cùng một đối tượng iterable. Mỗi iterator sẽ duy trì trạng thái tiến trình riêng. Do đó, bằng cách tạo nhiều thể hiện iterator từ một iterable, có thể lặp đến cuối cùng với một thể hiện trong khi thể hiện kia vẫn ở điểm bắt đầu.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Lưu ý iterator_b in ra phần tử đầu tiên của dãy.
Vì vậy, có thể nói iterator có bản chất lười: khi một iterator được tạo, các phần tử sẽ không được tạo ra cho đến khi được yêu cầu. Nói cách khác, các phần tử của list chỉ được trả về khi chúng ta yêu cầu rõ ràng bằng next(iter(list_instance)).
Tuy nhiên, có thể trích xuất tất cả giá trị từ một iterator cùng lúc bằng cách gọi một hàm tạo cấu trúc dữ liệu dựng sẵn dạng iterable (ví dụ, list(), set(), tuple()) trên đối tượng iterator để buộc nó sinh ra toàn bộ phần tử ngay lập tức.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Điều này không được khuyến nghị với các iterator lớn vì nó buộc mọi phần tử phải được tạo và giữ trong bộ nhớ cùng lúc, làm mất ý nghĩa của lazy evaluation.
Khi một bộ dữ liệu quá lớn để chứa thoải mái trong bộ nhớ, hoặc khi bạn muốn lặp lười mà không cần viết cả một lớp iterator, generator thường là lựa chọn phù hợp hơn.
Generators trong Python
Giải pháp nhanh nhất thay cho việc hiện thực một iterator là dùng generator. Mặc dù generator trông giống các hàm Python thông thường, chúng lại khác. Trước hết, một generator không trả về các phần tử. Thay vào đó, nó dùng từ khóa yield để sinh các phần tử ngay khi cần. Vì vậy, có thể nói generator là một kiểu hàm đặc biệt tận dụng đánh giá lười.
Generator không lưu trữ nội dung của chúng trong bộ nhớ như cách bạn kỳ vọng ở một iterable thông thường. Ví dụ, nếu mục tiêu là tìm tất cả ước số của một số nguyên dương, thông thường chúng ta sẽ hiện thực một hàm truyền thống (tìm hiểu thêm về Hàm trong Python trong hướng dẫn này) như sau:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
Đoạn mã trên trả về toàn bộ danh sách ước số. Tuy nhiên, hãy để ý sự khác biệt khi dùng generator thay cho hàm Python truyền thống:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Vì chúng ta dùng từ khóa yield thay cho return, hàm không thoát ngay sau khi chạy. Về bản chất, chúng ta bảo Python tạo một đối tượng generator thay vì một hàm truyền thống, cho phép theo dõi trạng thái của generator.
Do đó, có thể gọi hàm next() trên iterator lười để hiển thị các phần tử của dãy từng cái một.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Một cách khác để tạo generator là dùng generator comprehension. Biểu thức generator có cú pháp tương tự list comprehension, ngoại trừ việc dùng dấu ngoặc tròn thay vì vuông.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Khám phá từ khóa yield trong Python
Từ khóa yield điều khiển luồng của một hàm generator. Thay vì thoát khỏi hàm như khi dùng return, từ khóa yield trả về khỏi hàm nhưng ghi nhớ trạng thái của các biến cục bộ.
Generator được trả về từ lần gọi yield có thể được gán cho một biến và lặp qua bằng hàm next() – điều này sẽ thực thi hàm cho đến từ khóa yield đầu tiên mà nó gặp. Khi chạm tới yield, việc thực thi hàm bị tạm dừng. Khi đó, trạng thái của hàm được lưu lại. Nhờ vậy, chúng ta có thể tiếp tục thực thi hàm khi muốn.
Hàm sẽ tiếp tục từ vị trí gọi yield. Ví dụ:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
Trong đoạn mã trên, generator của chúng ta có bốn lần gọi yield, nhưng chúng ta thử gọi next năm lần, dẫn đến ngoại lệ StopIteration. Hành vi này xảy ra vì generator của chúng ta không phải là một chuỗi vô hạn, nên gọi quá số lần dự kiến sẽ làm cạn generator.
Tổng kết
Tóm lại, iterator là những đối tượng có thể được lặp, còn generator là các hàm đặc biệt tận dụng đánh giá lười. Khi tự hiện thực một iterator, bạn phải tạo phương thức __iter__() và __next__(), trong khi generator có thể được hiện thực bằng từ khóa yield trong một hàm Python hoặc comprehension.
Bạn có thể ưu tiên dùng iterator tùy chỉnh thay vì generator khi cần một đối tượng có hành vi duy trì trạng thái phức tạp hoặc khi bạn muốn cung cấp thêm các phương thức ngoài __next__(), __iter__() và __init__(). Mặt khác, generator có thể phù hợp hơn khi làm việc với các tập dữ liệu lớn vì chúng không lưu nội dung trong bộ nhớ, hoặc khi không cần thiết phải tự hiện thực một iterator.

FAQS
Sự khác biệt giữa iterator và generator trong Python là gì?
Iterator là bất kỳ đối tượng nào hiện thực __iter__() và __next__(). Generator là cách đơn giản hơn để tạo một iterator bằng một hàm với từ khóa yield. Mọi generator đều là iterator, nhưng không phải mọi iterator đều là generator.
Khi nào tôi nên dùng generator thay vì list trong Python?
Hãy dùng generator cho các dãy lớn hoặc vô hạn, hoặc khi hiệu quả bộ nhớ là quan trọng. List giữ mọi phần tử trong bộ nhớ cùng lúc, còn generator tạo ra từng giá trị theo nhu cầu. Với bộ dữ liệu nhỏ mà bạn sẽ dùng lại nhiều lần, list thường là ổn.
Từ khóa yield trong Python làm gì?
Từ khóa yield biến một hàm thành generator. Thay vì trả về và thoát, yield tạm dừng hàm, trả về một giá trị và ghi nhớ trạng thái để lần gọi tiếp theo có thể tiếp tục thực thi.
Làm thế nào để tạo một generator trong Python?
Hoặc viết một hàm dùng yield thay vì return, hoặc dùng generator expression — cú pháp giống list comprehension nhưng với dấu ngoặc tròn, như (x * 2 for x in range(10)).
Generators có nhanh hơn iterators trong Python không?
Không nhanh hơn về tốc độ thô, nhưng chúng hiệu quả bộ nhớ hơn vì tạo giá trị theo nhu cầu. Với dữ liệu lớn, điều đó thường mang lại hiệu năng tổng thể tốt hơn; với dữ liệu nhỏ, khác biệt là không đáng kể.