Hướng dẫn dùng itrable python

Trong các bài học trước đây bạn đã biết các kiểu dữ liệu cơ bản của Python như list, tuple. Bạn cũng biết về hàm range() và vòng lặp for. Tất cả chúng đều thực thi một mẫu thiết kế chung – mẫu iterator.

Nội dung chính

  • Collection và iteration
  • Ví dụ về xây dựng Iterable và Iterator class
  • Iterable trong Python
  • Iterator trong Python
  • Cách hoạt động của vòng lặp for
  • Kết luận

Iterator là một mẫu thiết kế rất quan trọng giúp bạn làm việc với dữ liệu. Ví dụ bạn có thể tạo ra các cấu trúc dữ liệu tuần tự mới. Các cấu trúc dữ liệu dạng này phù hợp cho việc tạo ra các chuỗi số, làm việc với file, hoặc giao tiếp qua mạng.

Collection và iteration

Trong Python và các ngôn ngữ khác có thể phân ra hai loại dữ liệu: loại dữ liệu chỉ chứa một giá trị (như các loại số và bool), và loại dữ liệu chứa nhiều giá trị.

Loại dữ liệu chứa nhiều giá trị bạn đã gặp bao gồm list, tuple, dict, string, set. Chúng được gọi chung là dữ liệu collection (tập hợp) hay container (hộp chứa).

Một đặc điểm phổ biến của container/collection là khả năng truy xuất lần lượt từng phần tử. Lấy ví dụ, bạn có thể lần lượt duyệt qua lần lượt từng phần tử của một danh sách (list) hoặc một tập hợp toán học (set).

Người ta gọi quá trình truy xuất lần lượt từng phần tử của container là iteration (vòng lặp). Tại mỗi thời điểm trong iteration, bạn chỉ có thể truy xuất một bộ phận của dữ liệu.

Dữ liệu dạng container có thể chia làm hai loại:

  1. Một số cho phép đánh chỉ số và truy xuất ngẫu nhiên đến từng phần tử, ví dụ như list, tuple, string. Các kiểu dữ liệu này được gọi là các kiểu dữ liệu sequence (kiểu dữ liệu tuần tự).
  2. Một số loại dữ liệu khác không cho đánh chỉ số và không thể truy xuất ngẫu nhiên như dict, set, file.

Khi làm việc với các loại dữ liệu dạng tập hợp như vậy bạn có thể hình dung theo hai kiểu: (1) toàn bộ dữ liệu đồng thời tải vào bộ nhớ, (2) tại mỗi thời điểm bạn chỉ tải và truy xuất một bộ phận của dữ liệu.

Bạn có thể so sánh thế này:

  • Khi đọc dữ liệu từ một file văn bản, bạn có thể lựa chọn tải toàn bộ văn bản vào bộ nhớ ngay lập tức, hoặc cũng có thể lựa chọn mỗi lần chỉ tải một đoạn văn bản (lấy ký tự /n làm mốc) để xử lý.
  • Khi tạo ra một dãy số (chẳng hạn chuỗi Fibonacci), bạn có thể lựa chọn tạo ra tất cả các số (trong một giới hạn nào đó) ngay lập tức, hoặc cũng có thể lựa chọn mỗi lần chỉ tạo ra một số, và chỉ tạo ra số khi chương trình cần đến (ví dụ, để in ra hoặc để tính toán).

Cách thứ nhất có lợi về tốc độ truy cập vì mọi thứ nằm hết trong bộ nhớ nhưng đồng thời lại tốn bộ nhớ hơn. Cách này đặc biệt không phù hợp cho việc xử lý lượng dữ liệu quá lớn hoặc dữ liệu không xác định được vị trí kết thúc (như đọc dữ liệu từ mạng hoặc tạo chuỗi số).

Cách thứ hai có nhược điểm về tốc độ truy xuất, vì mỗi khi cần truy xuất, dữ liệu mới được tạo ra hoặc được tải vào bộ nhớ. Tuy nhiên cách này có thể xử lý được lượng dữ liệu không giới hạn. Đây là cách Python lựa chọn để áp dụng cho các kiểu dữ liệu tập hợp như list, tuple, string, cũng như vận dụng trong vòng lặp for.

Các kiểu dữ liệu tập hợp trong Python cũng được gọi chung là iterable. Để sử dụng dữ liệu iterable, bạn cần đến một iterator tương ứng.

Ví dụ về xây dựng Iterable và Iterator class

Hãy cùng thực hiện ví dụ sau:

class FibonacciIterable:
    def __init__(self, count=10):
        self.count = count
    def __iter__(self):
        return FibonacciIterator(self.count)

class FibonacciIterator:
    def __init__(self, count=10):
        self.a, self.b = 0, 1
        self.count = count
        self.__i = 0
    def __next__(self):
        if self.__i > self.count:
            raise StopIteration()
        value = self.a
        self.a, self.b = self.b, self.a + self.b
        self.__i += 1
        return value
    # def __iter__(self):
    #     return self

def test1():
    print('Test 1:', end=' ')
    iterable = FibonacciIterable(15)
    for f in iterable:
        print(f, end=' ')
    print()

def test2():
    print('Test 2:', end=' ')
    iterator = FibonacciIterator(15)
    for f in iterator:
        print(f, end=' ')
    print()

def test3():
    print('Test 3:', end=' ')
    iterator = FibonacciIterator(15)
    while True:
        try:
            f = next(iterator)
            print(f, end=' ')
        except StopIteration:
            break
    print()

def test4():
    print('Test 4:', end=' ')
    iterable = FibonacciIterable(15)
    iterator = iter(iterable)
    while True:
        try:
            f = next(iterator)
            print(f, end=' ')
        except StopIteration:
            break
    print()

if __name__ == '__main__':
    test1()
    #test2()
    test3()
    test4()

Trong ví dụ này chúng ta xây dựng các class giúp tạo ra count phần tử đầu tiên của dãy số Fibonacci.

Nếu bạn chưa biết: dãy số Fibonacci có hai phần tử đầu tiên lần lượt là 0 và 1. Phần tử thứ ba trở đi có giá trị bằng tổng giá trị hai phần tử trước nó.

Kết quả của cả 3 hàm test là 15 giá trị đầu tiên của chuỗi Fibonacci:

Test 1: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610
Test 3: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610
Test 4: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610

Trong ví dụ fibonacci.py bạn xây dựng hai class, FibonacciIterable và FibonacciIterator. Hai class này lần lượt thuộc về hai loại khác biệt: iterable và iterator.

Sự phân chia iterable và iterator như vậy tuân theo mẫu thiết kế iterator. Mẫu thiết kế này giúp phân tách thùng chứa (container) với dữ liệu thực sự chứa trong nó. Trong đó, container là iterable, còn dữ liệu thực sự chính là iterator. Nghĩa là, cùng một container nhưng bạn có thể cho nó “chứa” dữ liệu khác biệt nhau.

Iterable trong Python

Iterable trong Python là loại object container/collection.

Theo mẫu thiết kế, về hình thức thì iterable có khả năng trả lại lần lượt từng giá trị. Nghĩa là trong client code bạn có thể lần lượt duyệt từng phần tử của nó.

Tuy nhiên, iterable thực tế chỉ đơn giản là một loại thùng chứa (container). Dữ liệu thực thụ sẽ do iterator tải hoặc tạo ra. Do vậy, mỗi iterable phải có một iterator tương ứng.

Về quy định, để tạo ra một iterable, trong class phải chứa phương thức __iter__(). Khi có mặt phương thức __iter__(), class tương ứng được coi là một iterable và có thể sử dụng trực tiếp trong vòng lặp for.

Phương thức __iter__() phải trả lại một object thuộc kiểu iterator. Phương thức này sẽ được gọi tự động để lấy iterator, ví dụ, khi sử dụng trong vòng lặp for.

Hãy nhìn lại code của FibonacciIterable bạn sẽ thấy các lý thuyết trên đều được áp dụng:

class FibonacciIterable:
    def __init__(self, count=10):
        self.count = count
    def __iter__(self):
        return FibonacciIterator(self.count)

Hàm tạo của class này nhận giá trị count – số lượng phần tử của dãy sẽ tạo.

Class này xây dựng phương thức __iter__(). Phương thức này chỉ làm nhiệm vụ trả lại một object của iterator FibonacciIterator.

Cũng để ý rằng, FibonacciIterable là class sẽ được sử dụng trong client code còn FibonacciIterator sẽ ở “hậu trường”. Người sử dụng sẽ chỉ biết đến iterable chứ không biết đến iterator. Như vậy, mặc dù client code cung cấp biến count cho iterable, thực tế giá trị này sẽ truyền sang cho iterator để sử dụng. Tự bản thân iterable không sử dụng.

Iterator trong Python

Iterator là loại object có nhiệm vụ tạo ra dữ liệu cho iterable. Tuy nhiên, tự bản thân iterator lại không phải là một kiểu dữ liệu container. Iterator phải phối hợp với iterable thì mới tạo ra được một container có dữ liệu và có thể duyệt được dữ liệu.

Tổ hợp iterable-iterator có thể hình dung giống như một chiếc thùng (iterable), và bạn có thể bỏ vào đó gạch hoặc ngói hoặc bất kỳ vật gì. Việc bỏ vào thùng vật gì sẽ do iterator quyết định.

Iterator object bắt buộc phải xây dựng phương thức __next__(). Nhiệm vụ của phương thức này là tạo ra phần tử cho iterable. Phương thức này cũng được gọi tự động nếu client code cần lấy dữ liệu. Trong phương thức __next__(), nếu không còn phần tử nào nữa thì cần phát ra ngoại lệ StopInteration.

Hãy xem lại code của lớp FibonacciIterator:

class FibonacciIterator:
    def __init__(self, count=10):
        self.a, self.b = 0, 1
        self.count = count
        self.__i = 0
    def __next__(self):
        if self.__i > self.count:
            raise StopIteration()
        value = self.a
        self.a, self.b = self.b, self.a + self.b
        self.__i += 1
        return value
    # def __iter__(self):
    #     return self

Khi khởi tạo object sẽ gán giá trị hai phần tử đầu tiên của dãy (a = 0, b =1), nhận giá trị biến count (số lượng phần tử cần tạo), và gán biến đếm __i = 0.

Mỗi lần gọi phương thức __next__() sẽ kiểm tra biến đếm. Nếu biến đếm vượt quá count sẽ phát ra ngoại lệ StopIteration. Đây là yêu cầu bắt buộc của __next__() nếu muốn kết thúc duyệt dữ liệu. Nếu biến đếm trong giới hạn thì trả lại giá trị hiện tại của a, đồng thời cập nhật giá trị mới cho a, b (a = b, còn b = a + b). Logic tạo ra a và b hoàn toàn theo định nghĩa của chuỗi Fibonacci.

Thông thường, trong iterator có thể thực thi cả __iter__() giúp một iterator cũng đồng thời là một iterable. Trong trường hợp này __iter__() của iterator chỉ cần trả lại chính object đó (self). Tuy nhiên, để giúp bạn dễ dàng phân biệt iterable và iterator, chúng ta tạm thời comment phương thức __iter__() trong iterator.

Cách hoạt động của vòng lặp for

Iterable là loại object mà bạn có thể trực tiếp sử dụng với vòng lặp for của Python.

Mặc dù bạn đã học cách sử dụng vòng lặp for trước đây. Tuy nhiên, hẳn bạn đã nhận ra rằng vòng lặp for của Python không thực sự giống như vòng lặp for của C/C++/Java hay C#. Vòng lặp for của Python, về hình thức, thì tương tự như for của các ngôn ngữ kiểu C nhưng lại hoạt động giống như foreach của C#/C++/Java.

Hãy xem lại hàm Test4:

def test4():
    print('Test 4:', end=' ')
    iterable = FibonacciIterable(15)
    iterator = iter(iterable)
    while True:
        try:
            f = next(iterator)
            print(f, end=' ')
        except StopIteration:
            break
    print()

Mặc dù ở đây sử dụng vòng lặp while, đó chính là mô phỏng hoạt động của vòng lặp for trên thực tế.

Logic hoạt động của vòng lặp for như sau:

  • Gọi hàm tích hợp iter(). Hàm này thực tế sẽ gọi __iter__() của iterable để lấy object iterator tương ứng.
  • Trên iterator liên tục gọi hàm next() để lấy dữ liệu cụ thể. Hàm này sẽ gọi tới __next__() của iterator.
  • Thực hiện các xử lý cần thiết trên dữ liệu vừa lấy được.
  • Nếu gặp ngoại lệ StopIteration thì dừng vòng lặp.

Bằng cách này bạn có thể tự mình tạo ra một ‘vòng lặp’ riêng thay cho for.

Dĩ nhiên, cách sử dụng iterable này rất cồng kệnh. Chúng ta đưa ra để bạn hiểu nguyên lý hoạt động của vòng for.

Trên thực tế, vòng lặp for của Python tự động thực hiện tất cả các thao tác trên nếu bạn cung cấp cho nó một iterable. Đây là trường hợp của hàm test1():

def test1():
    print('Test 1:', end=' ')
    iterable = FibonacciIterable(15)
    for f in iterable:
        print(f, end=' ')
    print()

Bạn tạo ra một object iterable từ class FibonacciIterable và cấp thẳng cho lệnh for. Lệnh for sẽ tự động thực hiện theo logic chúng ta đã chỉ ở trên. Mỗi lần gọi next() sẽ trả giá trị về cho biến tạm f để chúng ta sử dụng.

Trong trường hợp bạn không xây dựng iterable mà chỉ có iterator, bạn không thể trực tiếp sử dụng iterator trong vòng lặp for. Khi đó bạn phải sử dụng theo kiểu của hàm test3:

def test3():
    print('Test 3:', end=' ')
    iterator = FibonacciIterator(15)
    while True:
        try:
            f = next(iterator)
            print(f, end=' ')
        except StopIteration:
            break
    print()

Ở đây bạn phải làm thủ công với vòng lặp while theo đúng logic đã trình bày ở trên.

Như đã nói, iterator cũng thường xây dựng luôn cả phương thức __iter__() giúp cho iterator đồng thời đóng vai trò của iterable. Nếu bạn bỏ dấu comment của phương thức __iter__() trong FibonacciIterator, bạn có thể sử dụng hàm test2:

def test2():
    print('Test 2:', end=' ')
    iterator = FibonacciIterator(15)
    for f in iterator:
        print(f, end=' ')
    print()

Ở đây object iterator đồng thời đóng luôn vai trò của iterable. Do vậy bạn dùng được nó trong vòng lặp for.

Kết luận

Trong bài học này chúng ta đã học về iterable và iterator. Đây là một chủ đề khó và rất hay gây nhầm lẫn. Không có nhiều tài liệu trên mạng giải thích được rõ ràng vấn đề này.

  • Iterable và iterator là hai bộ phận của mẫu thiết kế iterator giúp phân tách thùng chứa (iterable) và dữ liệu (iterator).
  • Iterable được sử dụng bởi client code trong vòng lặp for; Iterator được vòng lặp này tự động tạo ra từ iterable và sử dụng để lấy dữ liệu.
  • Iterable phải thực thư phương thức __iter__() nhằm chỉ định iterator.
  • Iterator phải thực thi phương thức __nex__() để lấy dữ liệu.
  • Thông thường iterator cũng thực thi luôn cả __iter__(), do vậy iterator có thể đồng thời đóng vai trò iterable.

+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạn!