Reportar esta app

Description

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.

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 loại 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 contain 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.

Tham khảo thêm ngay  Tham số trong hàm trong Python

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:

fibonacci.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 = , 1
        self.count = count
        self.__i =

    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.

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

Trong ví dụ fibonacci.py bạn xây dựng hai class, Fibonacci Iterable 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 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.

Tham khảo thêm ngay  Module và package trong Python

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 tả 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à cso thể sử dụng trực tiếp 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:


1
2
3
4
5
6
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 FiboncacciIterator.

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 tâ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 ộ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:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FibonacciIterator:
    def __init__(self, count=10):
        self.a, self.b = , 1
        self.count = count
        self.__i =

    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.

Tham khảo thêm ngay  Giới thiệu chung về class trong Python, constructor

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 commment phương thưc s__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ự 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:


1
2
3
4
5
6
7
8
9
10
11
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():


1
2
3
4
5
6
def test1():
    print('Test 1:', end=' ')
    iterable = FibonacciIterable(15)
    for f in iterable:
        print(f, end=' ')
    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 commment của phương thức __iter__() trong FibonacciIterator, bạn có thể sử dụng hàm test2:


1
2
3
4
5
6
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.

Leave a Reply

Your email address will not be published. Required fields are marked *