天天看點

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象

1. 可疊代對象:

可疊代對象包括:

  1. 疊代器 (包括生成器)。
  2. 字元串 str,清單 list, 字典 dict,元組 tuple,集合 set。
  3. 實作了 __iter__ 方法的對象。

判斷一個變量是否為可疊代對象:

from collections import Iterable
isinstance(變量, Iterable)
           

測試代碼如下:

from collections import Iterable

l1 = [1, 2, 3]
t1 = (1, 2, 3)
d1 = {'key1': 'value1'}
str1 = 'James'
set1 = {'a', 'b', 'c'}


class MyObject:
    def __init__(self):
        pass


class MyIter:
    def __init__(self):
        pass

    def __iter__(self):
        yield 1


a = MyObject()
b = MyIter()

print(isinstance(l1, Iterable))
print(isinstance(t1, Iterable))
print(isinstance(d1, Iterable))
print(isinstance(str1, Iterable))
print(isinstance(set1, Iterable))
print(isinstance(a, Iterable))
print(isinstance(b, Iterable))
           

運作結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

特别注意變量 a 和 b,a 是 MyObject 的一個執行個體,但是由于沒有實作 __iter__ 方法,是以不是一個可疊代對象,而 b 是 MyIter 的一個執行個體并且實作了 __iter__ 方法,是以就是一個可疊代對象。

2. 疊代器

疊代器可以通過 next() 方法不斷重複擷取下一個值,直到所有元素全部輸出完之後,傳回 StopIteration才停止。同時實作在 __iter__ 和 __next__ 兩個函數的對象,就是疊代器。其中 __iter__ 方法需要傳回一個疊代器, 而 __next__ 方法傳回下一個傳回值或者 StopIteration。

判斷是否為疊代器:

from collections import Iterator
isinstance(變量, Iterator)
           

測試代碼如下:

from collections import Iterable
from collections import Iterator

l1 = [1, 2, 3]
t1 = (1, 2, 3)
d1 = {'key1': 'value1'}
str1 = 'James'
set1 = {'a', 'b', 'c'}


class MyObject:
    def __init__(self):
        pass


class MyIterable:
    def __init__(self):
        pass

    def __iter__(self):
        yield 1


class MyIterator:
    def __init__(self):
        pass

    def __iter__(self):
        yield 1

    def __next__(self):
        return self


a = MyObject()
b = MyIterable()
c = MyIterator()

print(isinstance(l1, Iterable))
print(isinstance(t1, Iterable))
print(isinstance(d1, Iterable))
print(isinstance(str1, Iterable))
print(isinstance(set1, Iterable))
print(isinstance(a, Iterable))
print(isinstance(b, Iterable))
print(isinstance(c, Iterable))

print('********************')

print(isinstance(l1, Iterator))
print(isinstance(t1, Iterator))
print(isinstance(d1, Iterator))
print(isinstance(str1, Iterator))
print(isinstance(set1, Iterator))
print(isinstance(a, Iterator))
print(isinstance(b, Iterator))
print(isinstance(c, Iterator))
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

主要關注第二部分,發現字元串 str,清單 list, 字典 dict,元組 tuple,集合 set,沒有實作 __iter__ MyObject 和僅實作 __iter__ 的 MyIterable 都不是疊代器,隻有實作了 __iter__ 和 __next__ 的 MyIterator 才是疊代器。

對于可疊代對象可以使用 iter() 轉為疊代器:

l2 = iter(l1)
t2 = iter(t1)
d2 = iter(d1)
str2 = iter(str1)
set2 = iter(set1)
b2 = iter(b)

print('*************************')

print(isinstance(l2, Iterator))
print(isinstance(t2, Iterator))
print(isinstance(d2, Iterator))
print(isinstance(str2, Iterator))
print(isinstance(set2, Iterator))
print(isinstance(b2, Iterator))
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

疊代器的優點:

  1. 提供了一種不依賴于索引的取值方式
  2. 惰性計算,節省記憶體

這裡的節省記憶體是指 疊代器在疊代過程中,不會一次性把可疊代對象的所有元素都加載到記憶體中,僅僅是在疊代至某個元素時才加載該元素,而在這之前或之後,元素可以不存在或者被銷毀。這個特點就使得疊代器适合用于周遊一些巨大的 或是 無限的集合。

疊代器的缺點:

  1. 取值不如按照索引取值來的友善(索引可以直接定位某一個值,疊代器不行,隻能一個一個地取下去)
  2. 疊代器隻能往後疊代,不能回退(執行 __next__ 方法隻能向後,不能向前)
  3. 無法擷取疊代器的長度

3. 生成器

生成器是一種特殊的疊代器,不過實作更加簡單。實作的方式有兩種:

  1. 生成器表達式
  2. 帶有 yield 關鍵字的函數

生成器作為一種特殊的疊代器,相比于一般疊代器更加優雅。它不需要再像上面的類一樣寫 __iter__ 和 __next__ 方法了,隻需要一個 yield 關鍵字或生成器表達式。

def fib(n):
    i, a, b = 0, 0, 1
    while i < n:
        yield b
        a, b = b, a + b
        i = i + 1


g1 = (x * x for x in range(1, 11))
g2 = fib(6)

print('********************')

print(isinstance(g1, Iterator))
print(isinstance(g2, Iterator))
for i in range(10):
    print(next(g1))
for i in range(6):
    print(next(g2))
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

Python的yield關鍵字的作用,就是把一個普通的函數變成生成器。當一個函數内出現yield關鍵字後,就會變異為生成器,其行為與普通函數不同。

從動态的角度,生成器在運作過程中:

  1. 當生成器函數被調用的時候,生成器函數不執行内部的任何代碼,直接立即傳回一個疊代器。
  2. 當所傳回的疊代器第一次調用 next 的時候,生成器函數從頭開始執行,如果遇到了執行 yield x,next立即傳回 yield 的值 x。
  3. 當所傳回的疊代器繼續調用next的時候,生成器函數從上次yield語句的下一句開始執行,直到遇到下一次執行yield。
  4. 任何時候遇到函數結尾,或者 return 語句,抛出 StopIteration 異常。

這裡關注一下第三點:

def g():
    print('L1')
    yield 1
    print('L2')
    yield 2
    print('L3')
    yield 3
    print('L4')


it = iter(g())
print('............')
v = next(it)
print(v)
v = next(it)
print(v)
v = next(it)
print(v)
v = next(it)
print(v)
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

可見 it = iter(g()) 語句之後生成器函數不執行内部的任何代碼,直接立即傳回一個疊代器直到第一次執行 next 這個時候才執行了 print(’…’),就從頭開始執行到 yield 語句,然後狀态挂起,等下一次執行 next 時,從上次的 yield 語句處繼續執行。

4. 惰性計算

我查找的資料上面對于生成器的惰性計算都是認可的,但有的認為疊代器不是惰性計算,有的認為是惰性計算。實踐出真理,我動手試驗了一下

import os
import psutil
import gc


class MyIterator2:
    def __init__(self, n):
        self.i = -1
        self.n = n

    def __iter__(self):
        return self

    def __next__(self):
        while (self.i + 1) < self.n:
            self.i = self.i + 1
            return self.i


class MyIterator3:
    def __init__(self, n):
        self.i = -1
        self.n = n

    def __iter__(self):
        return self

    def __next__(self):
        while (self.i + 1) < self.n:
            self.i = self.i + 1
            yield self.i


def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss/1024/1024
    print('{} memory used: {} M'.format(hint, memory))


def test_iterable():
    show_memory_info('initing iterable')
    my_iterable = [x * x for x in range(100000000)]
    show_memory_info('after iterable initiated')
    print('********')


def test_iterator1():
    show_memory_info('initing iterator1')
    my_iterable = [x * x for x in range(100000000)]
    my_iterator1 = iter(my_iterable)
    del my_iterable
    gc.collect()
    print(next(my_iterator1))
    show_memory_info('after iterator1 initiated')
    print('********')


def test_iterator2():
    show_memory_info('initing iterator2')
    my_iterator2 = MyIterator2(100000000)
    show_memory_info('after iterator2 initiated')
    print('********')


def test_iterator3():
    show_memory_info('initing iterator3')
    my_iterator3 = MyIterator3(100000000)
    show_memory_info('after iterator3 initiated')
    print('********')


def test_generator():
    show_memory_info('initing generator')
    my_generator = (x * x for x in range(100000000))
    show_memory_info('after generator initiated')
    print('********')


if __name__ == '__main__':
    test_iterable()
    test_iterator1()
    test_iterator2()
    test_iterator3()
    test_generator()
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

1. 對于可疊代對象,顯然不是惰性的,占用了大量記憶體。

2. 這個疊代器是用 iter() 方法将一個可疊代對象轉為一個疊代器,結果還是占用了大量記憶體,我一開始推測是由于一開始的可疊代對象占用了大量記憶體。但是我發現删除可疊代對象并回收記憶體之後還是占有了大量記憶體,是以我判定用這種方式生成的疊代器不是惰性的。

3. 手動定義 __iter__ 和 __next__ 生成的疊代器确實是惰性的,占用的記憶體大大減少。

4. 一開始我在解決生成器的記憶體減少是 yield 關鍵字的問題,結合 3 和 4 的對比,發現即使是 return 語句也是惰性的,是以惰性的關鍵不是 yield 的語句。

5. 生成器的惰性也是顯然地。

5. 疊代器和生成器隻供消耗一次

x = [0, 1, 2, 3, 4]
y = iter([0, 1, 2, 3, 4])
z = (i for i in range(5))

print(tuple(x))
print(tuple(x))
print(tuple(y))
print(tuple(y))
print(tuple(z))
print(tuple(z))
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

可以看到可疊代對象可以重複使用,而疊代器和生産器隻供消耗一次。

6 iter() 産生的疊代器

def scale(data, factor):
    for j in range(len(data)):
        data[j] *= factor


a_list = [1, 2, 3, 4, 5]
iterator1 = iter(a_list)
print(next(iterator1))
print(next(iterator1))
print(next(iterator1))
scale(a_list, 10)
print(next(iterator1))
print(next(iterator1))
           

結果如下:

Python:可疊代對象,疊代器,生成器。提綱:生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象5. 疊代器和生成器隻供消耗一次6 iter() 産生的疊代器總結

iter() 産生的疊代器儲存原始清單的目前索引,該索引指向下一個元素。是以,如果原始清單的内容在疊代器構造之後但在疊代完成之前被修改,疊代器将報告原始清單的更新内容。

總結

  1. 生成器 ⫋ \subsetneqq ⫋ 疊代器 ⫋ \subsetneqq ⫋ 可疊代對象。
  2. 任何可疊代對象都可以使用 for…in 語句。實際上在運作 for…in 時,由解釋器幫助我們對可疊代獨享調用了 iter() 方法,得到一個疊代器,然後每次使用 next 取一個值出來。
  3. 以清單 list 為例,list 本身隻有 __iter__ 方法,是以隻是一個可疊代對象,但是執行 for…in 的時候,調用 __iter__ 方法 傳回一個疊代器,此疊代器有 __next__ 方法,正在用來執行 for…in…
  4. 疊代器的适用的場景:不關心元素的随機通路,元素的個數不可提前預測。
  5. 使用 iter(可疊代對象) 生成的疊代器不是惰性的,而嚴格手動定義 __iter__ 和 __next__ 方法的疊代器是惰性的,且與使用 return 語句和 yield 語句無關。
  6. 生成器是惰性的。
  7. 生成器使用 yield 語句更加優雅,但本身就是一種特殊的疊代器。
  8. 有的資料提到的生成器的延時操作可以用惰性計算了解:延遲操作就是在需要的時候才産生結果,不是立即産生結果,顯然和惰性計算本質是類似的。