Hướng dẫn iterator vs iterable python
Khi tìm hiểu cách sử dụng các kiểu dữ liệu có nhiều phần tử như array, list, v.v. trong các ngôn ngữ lập trình hiện đại, chúng ta thường gặp các từ khóa như Iterable, Iterator, Enumerator … Dù rằng các khái niệm do các từ khóa này đưa ra không phải là phức tạp lắm, nhưng đôi khi chúng sẽ gây ra “nhức đầu, chóng mặt” cho các lập trình viên mới. Vì vậy, chúng ta sẽ tìm hiểu các khái niệm này một cách chi tiết trong bài viết này. Show Chúng ta sẽ bắt đầu với một vấn đề nhập môn: nếu được yêu cầu để lập trình một đoạn mã để duyệt tuần tự qua mọi phần tử trong một tập hợp nhiều phẩn tử, bạn sẽ làm thế nào? Với Python, đây là công việc rất đơn giản, chúng ta sẽ sử dụng vòng lặp for như sau:
Tiếp theo, chúng ta sẽ có một vấn đề khá thú vị: nếu được yêu cầu để làm công việc như trên mà không sử dụng cấu trúc lặp for, bạn sẽ làm thế nào? Là một lập trình viên, có lẽ điều đầu tiên chúng ta nghĩ đến là một vòng lặp có điều kiện theo truyền thống tương tự như đoạn mã dưới đây:
Đây là cú pháp bắt nguồn từ ngôn ngữ C. Tuy nhiên, nếu sử dụng cấu trúc lặp này trong Python, chúng ta sẽ khám phá ra rằng nó chỉ hoạt động với một số kiểu dữ liệu nhất định như list (danh sách hoặc mảng), string (chuỗi) nhưng lại không hoạt động được với một số kiểu dữ liệu như là dictionary (từ điển) hoặc set (tập hợp). Ví dụ như khi dùng cấu trúc lặp này với một set, chúng ta sẽ gặp lỗi như sau:
Theo gợi ý của thông báo lỗi từ đoạn mã trên, vấn đề là do kiểu dữ liệu “set” không hỗ trợ cho index (chỉ mục). Nếu phân tích kỹ hơn, chúng ta sẽ thấy rằng các kiểu dữ liệu có nhiều phẩn tử có sẵn trong Python được phân thành hai nhóm: dữ liệu kiểu tuần tự (sequence) và tập hợp (collection). Về bản chất, các dữ liệu kiểu sequence là các dữ liệu mà các phần tử trong đó được lập chỉ mục hoặc hiểu nôm na là được đánh số thứ tự từ 0 đến n (với n là độ dài của nhóm trừ đi 1). Còn các dữ liệu kiểu tập hợp là các dữ liệu không có chỉ mục. Trong Python, danh sách (list), chuỗi (string) và tuples là các kiểu dữ liệu thuộc nhóm thứ nhất. Còn các kiểu dữ liệu từ điển (dictionary), tập hợp (set) và một số kiểu dữ liệu khác thuộc nhóm thứ hai. Vậy làm sao chúng ta có thể tạo ra một cấu trúc lặp có thể vượt qua giới hạn này và có thể áp dụng cho cả hai nhóm sequence lẫn collection? Để làm được điều này, chúng ta cần hiểu về cơ chế duyệt tuần tự qua các khái niệm iterable và iterator. Iterable là gì?Hiểu một cách đơn giản, một iterable trong Python là một đối tượng cho phép bạn duyệt qua các phần tử của nó với vòng lặp for. Các đối tượng iterable không cần phải có chỉ mục, không cần phải có độ dài, thậm chí không cần phải hữu hạn. Đặc điểm tương đồng duy nhất của các đối tượng này là chúng có chứa nhiều hơn một phần tử. Sau đây là một ví dụ về một đối tượng iterable vô hạn có chứa các bội số của 3:
Chúng ta sẽ dùng vòng lặp for để duyệt qua từng phần tử của đối tượng này như sau:
Nếu chúng ta bỏ lệnh break ra khỏi đoạn mã trên, vòng lặp sẽ được thi hành vô hạn. Như vậy, các đối tượng iterable có thể có chiều dài vô hạn, và hệ quả là không phải lúc nào chúng ta cũng có thể chuyển đổi các đối tượng này về một đối tượng kiểu list (danh sách) hoặc bất kỳ kiểu đối tượng nào thuộc nhóm sequence. Do đó, chúng ta phải tìm cách nào đó để các đối tượng iterable có thể trả về từng phần tử trong chúng tương tự như cách làm việc của vòng lặp for. Và điều này dẫn chúng ta đến khái niệm tiếp theo là iterator. Tham khảo các tài liệu về Python, chúng ta sẽ thấy rằng các đối tượng iterable đều trả về một đối tượng iterator khi chúng ta đưa chúng vào phương thức iter() có sẵn trong Python như trong ví dụ sau:
Như vậy iterator là gì? Theo định nghĩa từ Python Wiki, một iterator là một đối tượng với một công việc duy nhất là trả về phần tử tiếp theo (“next”) trong các đối tượng iterable hoặc một ngoại lệ Chúng ta có thể nhận được một đối tượng iterator từ bất cứ đối tượng iterable nào:
Các đối tượng iterator cũng có thể được truyền vào phương thức
Iterator cũng là IterableNhư vậy, phương thức Thật ra vẫn còn một điểm đáng lưu ý: chúng ta có thể truyền một iterator cho phương thức
Điều này dẫn đến một số kết quả khá thú vị mà chúng ta sẽ không thảo luận trong bài viết này. Chúng ta sẽ trở lại vấn đề này trong một bài viết tương lai. Giao thức IteratorGiao thức iterator thật ra chỉ là một cách gọi hoa mỹ cho một khái niệm đơn giản là cách làm việc của iterable trong Python. Từ góc độ của Python, chúng ta có thể định nghĩa iterable và iterator theo cách làm việc như sau: Các đối tượng Iterable:
Các đối tượng Iterator:
Các định nghĩa này cũng đúng theo chiều ngược lại, có nghĩa là:
Vòng lặp bằng IteratorĐến đây, với những gì chúng ta đã biết về các iterable và iterator, chúng ta có thể tạo ra một kiểu vòng lặp tương tự như for nhưng lại không dùng đến vòng lặp for như trong ví dụ dưới đây. Vòng lặp
Chúng ta có thể gọi hàm này với bất kỳ đối tượng iterable nào và duyệt qua các phần tử chứa trong nó:
Hàm
Vòng lặp Giao thức iterator được sử dụng trong vòng lặp for, tuple unpacking (truy cập nhiều giá trị của tuple cùng lúc), list comprehension và các phương thức khác trong các thư viện mặc định của Python có thể được sử dụng với các đối tượng iterable. Sử dụng giao thức iterator là cách tổng quát nhất để duyệt qua bất kỳ đối tượng iterable nào trong Python. Cách xây dựng đối tượng IterableĐến đây, chúng ta đã hiểu khá rõ về iterable. Vậy làm cách nào chúng ta có thể định nghĩa các đối tượng iterable mới ngoài các đối tượng iterable đã có sẵn trong Python? Để tạo ra một đối tượng iterable, chúng ta sẽ cần thực hiện các công việc sau:
Ví dụ như chúng ta có một lớp classroom.py:Module cho một lớp iterable mới
Trong lớp Tiếp theo, chúng ta cần định nghĩa lớp classroomIterator.py: Module cho iterator của lớp ClassRoom
Trong lớp Đến đây, chúng ta có thể kiểm tra hoạt động của lớp iterable mới và iterator tương ứng với Python shell:
Kết quả đúng như chúng ta mong đợi: vòng lặp for sẽ duyệt qua từng phần tử trong đối tượng Generator là gì?Như vậy chúng ta đã hiểu thấu đáo về iterable, iterator và cách làm việc của vòng lặp for và có thể yên tâm khi sử dụng các đối tượng này rồi phải không? Thật không may là vẫn còn một chủ đề nữa cần được thảo luận trước khi chúng ta có thể hoàn tất phần này. Bây giờ bạn hãy thử hình dung điều gì sẽ xảy ra nếu bạn sử dụng vòng lặp for để đọc một file văn bản theo định dạng csv vào một danh sách như trong ví dụ sau: Đọc file csv theo từng dòng
Với phương thức csv_reader được định nghĩa như sau: Phương thức csv_reader()
Phương thức này sẽ đọc file và trả về một danh sách với mỗi phần tử là một dòng trong file. Nó sẽ làm việc tốt với các file nhỏ. Tuy nhiên, khi thi hành đoạn mã này với một file có kích thước rất lớn, bạn sẽ gặp kết quả như sau:
Điều gì đã xảy ra? Rất đơn giản, khi chúng ta mở file bằng phương thức Để giải quyết vấn đề này, Python cung cấp một giải pháp khá đơn giản, chúng ta chỉ cần sửa đổi lại phương thức
Với thay đổi này, chúng ta mở file, lần lượt đọc các dòng trong file, và mỗi khi đọc một dòng, chúng ta “nhường lại” (yield) dòng đó. Khi chạy đoạn mã mới này, chúng ta sẽ nhận được kết quả tương tự như sau và không có lỗi phát sinh: Như vậy chính xác là chúng ta đã làm gì với thay đổi trên? Chúng ta vừa chuyển đổi hàm csv_reader() thành một hàm generator (generator function). Phiên bản mới của hàm sẽ mở file, đi tuần tự qua các dòng trong file và trả về mỗi lần một dòng thay vì tất cả các dòng cùng lúc. Hàm generator được giới thiệu từ PEP 255. Các hàm thuộc loại này có tác dụng như các iterator nhưng có thể được xây dựng với cú pháp đơn giản hơn nhiều so với các iterator. Và cũng như các iterator, các giá trị do hàm generator trả về sẽ được đánh giá theo phương pháp đánh giá trì hoãn (lazy evaluation). Lazy evaluation là một kỹ thuật cho phép các giá trị trả về của các hàm chỉ được tạo ra khi chúng được sử dụng đến chứ không phải ở thời điểm hàm được gọi. Một lợi thế của kỹ thuật này là các iterator chỉ trả về mỗi lần một phần tử và không lưu các phần tử này vào bộ nhớ trong quá trình duyệt. Điều này cho phép các iterator duyệt qua các nhóm phần tử có số lượng rất lớn mà không bị ràng buộc về giới hạn bộ nhớ. Với Python, phương pháp này cho phép các iterator hoạt động được ngay cả với các nhóm phần tử có độ dài không giới hạn. Các phương pháp tạo ra generatorĐể tạo ra các generator, chúng ta có hai phương pháp:
Lợi ích của việc sử dụng generator
So với iterator thì mã cho generator ngắn gọn và dễ hiểu hơn nhiều phải không?
Giả sử chúng ta có một log file ghi nhận dữ liệu từ một chuỗi cửa hàng fastfood nổi tiếng. Mỗi hàng trong log file này được phân thành nhiều cột. Trong đó cột thứ sáu là số hamburger bán ra mỗi giờ. Chúng ta muốn tìm ra tổng số bánh bán ra trong toàn bộ file. Giả sử dữ liệu trong file được ghi nhận dưới dạng chuỗi và các số liệu không được
nhập vào sẽ được đánh dấu bằng chuỗi ‘
Các khái niệm tương đương trong Java và .NETJava có hỗ trợ cho Iterable với interface .NET cho phép định nghĩa một lớp tập hợp các phần tử (collection class)
là iterable khi lớp đó được tạo ra với các interface IEnumerable và Trong khuôn khổ bài viết này, chúng ta sẽ không đi sâu vào chi tiết của các đối tượng này trong các ngôn ngữ khác. Kết luậnHy vọng bài viết này sẽ giải đáp các câu hỏi của bạn về các khái niệm Iterable, Iterator và Generator và cách hoạt động của chúng trong Python nói riêng và một số ngôn ngữ lập trình khác nói chung. |