Trong Python, giao thức lặp (iterator protocol) và việc đảo ngược đối tượng được thực thi thông qua các phương thức đặc biệt (magic methods). Việc hiểu rõ cách vận hành của các phương thức này giúp chúng ta tùy biến hành vi của các lớp (class) khi làm việc với các cấu trúc vòng lặp.
Dưới đây là ba phương thức cốt lõi:
1. `__iter__(self)`: Phương thức này được gọi khi bạn cần khởi tạo một trình lặp (iterator) cho một container. Nó phải trả về chính đối tượng iterator đó. Thông thường, iterator chính là chính nó (self).
2. `__next__(self)`: Phương thức này chịu trách nhiệm trả về phần tử tiếp theo từ container. Khi không còn phần tử nào để trả về, nó phải ném ra ngoại lệ `StopIteration`.
3. `__reversed__(self)`: Được gọi bởi hàm tích hợp `reversed()`. Nó trả về một trình lặp mới để duyệt qua các phần tử của container theo thứ tự ngược lại.
Sử dụng iter và next để tạo trình lặp tùy chỉnh
Để một đối tượng có thể sử dụng trong vòng lặp `for`, nó cần triển khai cả `__iter__` và `__next__`.
class StepCounter:
def __init__(self, limit):
self.limit = limit
self.current = 0
def __iter__(self):
# Khởi tạo lại trạng thái khi bắt đầu vòng lặp
self.current = 0
return self
def __next__(self):
if self.current < self.limit:
self.current += 1
return self.current
else:
raise StopIteration
# Sử dụng
counter = StepCounter(5)
for num in counter:
print(num)
Kết quả đầu ra:
1
2
3
4
5
Tìm hiểu về reversed và cơ chế của vòng lặp for
Khi sử dụng hàm `reversed()`, Python sẽ tìm kiếm phương thức `__reversed__`. Tuy nhiên, để kết quả của `reversed()` có thể dùng được trong vòng lặp `for`, đối tượng trả về phải là một iterator (tức là phải có `__iter__`).
class Countdown:
def __init__(self, start_val):
self.start_val = start_val
self.index = 0
def __reversed__(self):
self.index = self.start_val
return self
def __next__(self):
if self.index > 0:
val = self.index
self.index -= 1
return val
else:
raise StopIteration
# Khởi tạo đối tượng
cd = Countdown(3)
rev_iter = reversed(cd)
# Gọi trực tiếp next() hoạt động bình thường
print(next(rev_iter))
print(next(rev_iter))
# Đoạn mã dưới đây sẽ báo lỗi: TypeError: 'Countdown' object is not iterable
# Vì class Countdown chưa định nghĩa __iter__
# for i in rev_iter:
# print(i)
Lý do lỗi: Vòng lặp `for` trong Python luôn gọi hàm `iter()` lên đối tượng mà nó nhận được. Nếu `__reversed__` trả về `self` nhưng `self` không có phương thức `__iter__`, vòng lặp sẽ thất bại dù `__next__` đã được định nghĩa.
Kết hợp linh hoạt iter, reversed và next
Chúng ta có thể sử dụng một biến trạng thái (flag) để điều khiển hướng duyệt dữ liệu bên trong cùng một phương thức `__next__`.
class DataStream:
def __init__(self, size):
self.size = size
self.cursor = 0
self.is_forward = True
def __iter__(self):
self.cursor = 0
self.is_forward = True
return self
def __reversed__(self):
self.cursor = 0
self.is_forward = False
return self
def __next__(self):
if self.cursor >= self.size:
raise StopIteration
# Logic tính toán giá trị dựa trên hướng duyệt
if self.is_forward:
value = self.cursor + 1
else:
value = self.size - self.cursor
self.cursor += 1
return value
stream = DataStream(3)
print("--- Duyệt xuôi với iter ---")
for x in stream:
print(x)
print("--- Duyệt ngược với reversed ---")
# Lưu ý: reversed(stream) trả về chính stream nhưng đổi flag is_forward
# Để dùng được trong for, stream phải có __iter__
rev_stream = reversed(stream)
for x in rev_stream:
print(x)
Kết quả vận hành:
--- Duyệt xuôi với iter ---
1
2
3
--- Duyệt ngược với reversed ---
3
2
1
Trong ví dụ cuối cùng, khi gọi `for x in reversed(stream)`, trình tự thực thi sẽ là:
1. `reversed(stream)` được gọi, kích hoạt `__reversed__`, đặt `is_forward = False` và trả về đối tượng `stream`.
2. Vòng lặp `for` gọi `iter()` lên đối tượng vừa nhận được. Nếu `__iter__` reset lại `is_forward = True`, logic duyệt ngược sẽ bị ghi đè. Do đó, trong các ứng dụng thực tế, người ta thường tách biệt iterator xuôi và ngược thành các lớp khác nhau hoặc xử lý logic `__iter__` cẩn thận hơn để tránh xung đột trạng thái.