天天看點

Python技術進階——疊代器、可疊代對象、生成器總覽容器疊代器可疊代對象生成器總結

http://kaito-kidd.com/2018/04/18/python-advance-iterator-generator/#more

在Python開發中,我們見過很常見的概念,如容器、可疊代對象、疊代器、生成器。

這些概念之間有什麼聯系和差別呢?

總覽

容器(container)、可疊代對象(iterable)、疊代器(iterator)、生成器(generator)的關系如下圖:

Python技術進階——疊代器、可疊代對象、生成器總覽容器疊代器可疊代對象生成器總結
  • list、set、tuple、dict都是容器
  • 容器通常是一個可疊代對象
  • 但凡可以傳回一個疊代器的對象,都稱之為可疊代對象
  • 實作了疊代器協定方法的稱作一個疊代器
  • 生成器是一種特殊的疊代器

    容器

    簡單來說,容器就是存儲一些事物的概念統稱,它最大的特性就是給你一個事物,告訴我這個事物是否在這個容器内。

在Python中,使用in或not in來判斷某個事物是存在或不存在某個容器内。

也就是說一個對象實作了contains方法,我們都可以稱之為容器。

在Python中,str、list、tuple、set、dict都是容器,因為我們可以用in或not in文法得知某個元素是否在容器内,它們内部都實作了contains方法。

print 1 in [1, 2, 3]    # True
print 2 not in (1, 2, 3)        # False
print 'a' in ('a', 'b', 'c')    # True
print 'x' in 'xyz'      # True
print 'a' not in 'xyz'      # True           

如果我們想自定義一個容器,隻需像下面這樣:

class A(object):

    def __init__(self):
        self.items = [1, 2]

    def __contains__(self, item):   # x in y
        return item in self.items

a = A()
print 1 in a    # True
print 2 in a    # True
print 3 in a    # False           

但一個容器不一定支援輸出存儲在内的所有元素的功能。

一個容器要想輸出儲存在内的所有元素,其内部需要實作疊代器協定。

疊代器

一個對象如果實作了疊代器協定,就可以稱之為疊代器。

在Python中實作疊代器協定,需要實作以下2個方法:

  • iter,這個方法傳回對象本身
  • Python2中實作next,Python3中實作next,這個方法每次傳回疊代的值,在沒有可疊代元素時,抛出StopIteration異常

    下面我們實作一個自定義的疊代器:

class A(object):
    """内部實作了疊代器協定,這個對象就是一個疊代器"""
    def __init__(self, n):
        self.idx = 0
        self.n = n

    def __iter__(self):
        print '__iter__'
        return self

    def next(self):
        if self.idx < self.n:
            val = self.idx
            self.idx += 1
            return val
        else:
            raise StopIteration()

# 疊代元素
a = A(3)
for i in a:
    print i
print '-------'
# 再次疊代,沒有元素輸出,疊代器隻能疊代一次
for i in a:
    print i

# __iter__
# 0
# 1
# 2
# -------
# __iter__           

在執行for循環時,我們看到iter的列印被輸出,然後依次輸出next中的元素。

其實在執行for循環時,實際調用順序是這樣的:

for i in a => b = iter(a) => next(b) => next(b) ... => StopIteration => end           

首選執行iter(a),iter會調用iter,在得到一個疊代器後,循環執行next,next會調用疊代器的next,在遇到StopIteration異常時,停止疊代。

但注意,再次執行疊代器,如果所有元素都已疊代完成,将不會再次疊代。

如果我們想每次執行都能疊代元素,隻需在疊代時,執行的都是一個新的疊代器即可:

for i in A(3):
    print i

# 每次執行一個新的疊代對象
for i in A(3):
    print i           

可疊代對象

但凡是可以傳回一個疊代器的對象,都可以稱之為可疊代對象。

這句話怎麼了解?

可以翻譯為:iter方法傳回疊代器,這個對象就是可疊代對象。

我們在上面看到的疊代器,也就是說實作了iter和next/next方法的類,這些類的執行個體就是一個可疊代對象。

疊代器一定是個可疊代對象,但可疊代對象不一定是疊代器。

這句話怎麼了解?我們看代碼:

class A(object):
    """
    A的執行個體不是疊代器,因為隻A實作了__iter__
    但這個類的執行個體是一個可疊代對象
    因為__iter__傳回了B的執行個體,也就是傳回了一個疊代器,因為B實作了疊代器協定
    傳回一個疊代器的對象都被稱為可疊代對象
    """
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return B(self.n)

class B(object):
    """這個類是個疊代器,因為實作了__iter__和next方法"""
    def __init__(self, n):
        self.idx = 0
        self.n = n

    def __iter__(self):
        return self

    def next(self):
        if self.idx < self.n:
            val = self.idx
            self.idx += 1
            return val
        else:
            raise StopIteration()

b = B(3)        # b是一個疊代器,同時b是一個可疊代對象
for i in b:
    print i
print iter(b)   # <__main__.B object at 0x10eb95450>

a = A(3)        # a不是疊代器,但a是可疊代對象,它把疊代細節交給了B,B的執行個體是疊代器
for i in a:
    print i
print iter(a)   # <__main__.B object at 0x10eb95550>           

對于B:

  • B的執行個體是一個疊代器,因為其實作了疊代器協定iter和next方法
  • 同時B的iter方法傳回了self執行個體本身,也就是說傳回了一個疊代器,是以B的執行個體b也是一個可疊代對象

對于A:

  • A的執行個體不是一個疊代器,因為沒有同時滿足iter和next方法
  • 由于A的iter傳回了B的執行個體,而B的執行個體是一個疊代器,是以A的執行個體a是一個可疊代對象,換句話說,A把疊代細節交給了B

其實我們使用的内置對象list、tuple、set、dict,都叫做可疊代對象,但不是一個疊代器,因為其内部都把疊代細節交給了另外一個類,這個類才是真正的疊代器:

l = [1, 2]      # list是可疊代對象
iter(l)         # list傳回的疊代器是listiterator
<listiterator object at 0x10599c350>    
iter(l).next()  # 疊代器有next方法
1

t = ('a', 'b')  # tuple是可疊代對象
iter(t)         # tuple傳回的疊代器是tupleiterator
<tupleiterator object at 0x10599c390>   
iter(t).next()  # 疊代器有next方法
a

s = {1, 2}      # set是可疊代對象
iter(s)         # set傳回的疊代器是setiterator
<setiterator object at 0x10592df50> # 
iter(s).next()  # 疊代器有next方法
1

d = {'a': 1, 'b': 2}    # dict是可疊代對象
iter(d)                 # dict傳回的疊代器是dictionary-keyiterator
<dictionary-keyiterator object at 0x105977db8>
iter(d).next()          # 疊代器有next方法
a           

生成器

生成器是特殊的疊代器,它也是個可疊代對象。

有2種方式可以建立一個生成器:

  • 生成器表達式
  • 生成器函數

生成器表達式如下:

g = (i for i in range(5))   # 建立一個生成器
g
<generator object <genexpr> at 0x101334f50>
iter(g)         # 生成器就是一個疊代器
<generator object <genexpr> at 0x101334f50>
for i in g:     # 生成器也是一個可疊代對象
    print i
# 0 1 2 3 4           

生成器函數,包含yield關鍵字的函數:

def gen(n):
    # 生成器函數
    for i in range(n):
        yield i

g = gen(5)      # 建立一個生成器
print g         # <generator object gen at 0x10bb46f50>
print type(g)   # <type 'generator'>

# 疊代
for i in g:
    print i
# 0 1 2 3 4           

一般情況下,我們使用比較多的情況是以函數的方式建立生成器,也就是函數中使用yield關鍵字。

這個函數與包含return的函數執行機制不同:

  • 包含return的方法會以return關鍵字為終結傳回,每次執行都傳回相同的結果
  • 包含yield的方法一般用于疊代,每次執行遇到yield即傳回yield後的結果,但内部會保留上次執行的狀态,下次疊代繼續執行yield之後的代碼,直到再次遇到yield并傳回

當我們想得到一個很大的集合時,如果使用普通方法,一次性生成出這個集合,然後return傳回:

def gen_data(n):
    return [i for i in range(n)]    # 一次性生成大集合           

但如果這個集合非常大時,就需要在記憶體中一次性占用非常大的記憶體。

使用yield能夠完美解決這類問題,因為yield是懶惰執行的,一次隻會傳回一個值:

for gen_data(n):
    for i in range(n):
        yield i         # 每次隻生成一個元素           

生成器在Python中還有更大的用處,我們來看生成器中還有哪些方法:

g = (i for i in range(3))
dir(g)
['__class__', '__init__', '__iter__', 'next', 'send', 'throw' ...           

我們發現生成器中還包含了一個叫做send的方法,如何使用?

def gen(n):
    for i in range(n):
        a = yield i
        if a == 100:
            print 'a is 100'

a = gen(10)         # 建立生成器
print a.next()      # 0
print a.next()      # 1
print a.send(100)   # send(100)指派給a 然後列印出'a is 100'
print a.next()      # 2           

生成器允許在執行時,外部自定義一個值傳入生成器内部,進而影響生成器的執行結果。

總結

  • 疊代器必須實作疊代器協定iter和next/__next方法
  • iter傳回疊代器的對象稱作可疊代對象
  • 疊代器一定是個可疊代對象,但可疊代對象不一定是疊代器,有可能疊代細節傳遞給另一個類,這個類才是疊代器
  • 生成器一定是一個疊代器,同時也是個可疊代對象
  • 生成器是一種特殊的疊代器,yield關鍵字可實作懶惰計算,并使得外部影響生成器的執行成為了可能