鍍金池/ 教程/ Python/ 迭代器與生成器
類與對象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

迭代器與生成器

迭代是 Python 最強(qiáng)大的功能之一。初看起來,你可能會(huì)簡單的認(rèn)為迭代只不過是處理序列中元素的一種方法。 然而,絕非僅僅就是如此,還有很多你可能不知道的, 比如創(chuàng)建你自己的迭代器對象,在 itertools 模塊中使用有用的迭代模式,構(gòu)造生成器函數(shù)等等。 這一章目的就是向你展示跟迭代有關(guān)的各種常見問題。

手動(dòng)遍歷迭代器

問題

你想遍歷一個(gè)可迭代對象中的所有元素,但是卻不想使用 for 循環(huán)。

解決方案

為了手動(dòng)的遍歷可迭代對象,使用 next()函數(shù)并在代碼中捕獲 StopIteration 異常。 比如,下面的例子手動(dòng)讀取一個(gè)文件中的所有行:

def manual_iter():
    with open('/etc/passwd') as f:
        try:
            while True:
                line = next(f)
                print(line, end='')
        except StopIteration:
            pass

通常來講, StopIteration 用來指示迭代的結(jié)尾。 然而,如果你手動(dòng)使用上面演示的 next()函數(shù)的話,你還可以通過返回一個(gè)指定值來標(biāo)記結(jié)尾,比如 None。 下面是示例:

with open('/etc/passwd') as f:
    while True:
        line = next(f)
        if line is None:
            break
        print(line, end='')

討論

大多數(shù)情況下,我們會(huì)使用 for 循環(huán)語句用來遍歷一個(gè)可迭代對象。 但是,偶爾也需要對迭代做更加精確的控制,這時(shí)候了解底層迭代機(jī)制就顯得尤為重要了。

下面的交互示例向我們演示了迭代期間所發(fā)生的基本細(xì)節(jié):

>>> items = [1, 2, 3]
>>> # Get the iterator
>>> it = iter(items) # Invokes items.__iter__()
>>> # Run the iterator
>>> next(it) # Invokes it.__next__()
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration
>>>

本章接下來幾小節(jié)會(huì)更深入的講解迭代相關(guān)技術(shù),前提是你先要理解基本的迭代協(xié)議機(jī)制。 所以確保你已經(jīng)把這章的內(nèi)容牢牢記在心中。

代理迭代

問題

你構(gòu)建了一個(gè)自定義容器對象,里面包含有列表、元組或其他可迭代對象。 你想直接在你的這個(gè)新容器對象上執(zhí)行迭代操作。

解決方案

實(shí)際上你只需要定義一個(gè) __iter__() 方法,將迭代操作代理到容器內(nèi)部的對象上去。比如:

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

# Example
if __name__ == '__main__':
    root = Node(0)
    child1 = Node(1)
    child2 = Node(2)
    root.add_child(child1)
    root.add_child(child2)
    # Outputs Node(1), Node(2)
    for ch in root:
        print(ch)

在上面代碼中, __iter__()方法只是簡單的將迭代請求傳遞給內(nèi)部的 _children屬性。

討論

Python 的迭代器協(xié)議需要__iter__()方法返回一個(gè)實(shí)現(xiàn)了 __next__()方法的迭代器對象。 如果你只是迭代遍歷其他容器的內(nèi)容,你無須擔(dān)心底層是怎樣實(shí)現(xiàn)的。你所要做的只是傳遞迭代請求既可。

這里的iter() 函數(shù)的使用簡化了代碼,iter(s)只是簡單的通過調(diào)用 s.__iter__()方法來返回對應(yīng)的迭代器對象, 就跟 len(s) 會(huì)調(diào)用 s.__len__()原理是一樣的。

使用生成器創(chuàng)建新的迭代模式

問題

你想實(shí)現(xiàn)一個(gè)自定義迭代模式,跟普通的內(nèi)置函數(shù)比如 range(), reversed()不一樣。

解決方案

如果你想實(shí)現(xiàn)一種新的迭代模式,使用一個(gè)生成器函數(shù)來定義它。 下面是一個(gè)生產(chǎn)某個(gè)范圍內(nèi)浮點(diǎn)數(shù)的生成器:

def frange(start, stop, increment):
    x = start
    while x < stop:
        yield x
        x += increment

為了使用這個(gè)函數(shù), 你可以用 for 循環(huán)迭代它或者使用其他接受一個(gè)可迭代對象的函數(shù)(比如 sum(), list() 等)。示例如下:

>>> for n in frange(0, 4, 0.5):
...     print(n)
...
0
0.5
1.0
1.5
2.0
2.5
3.0
3.5
>>> list(frange(0, 1, 0.125))
[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875]
>>>

討論

一個(gè)函數(shù)中需要有一個(gè) yield 語句即可將其轉(zhuǎn)換為一個(gè)生成器。 跟普通函數(shù)不同的是,生成器只能用于迭代操作。 下面是一個(gè)實(shí)驗(yàn),向你展示這樣的函數(shù)底層工作機(jī)制:

>>> def countdown(n):
...     print('Starting to count from', n)
...     while n > 0:
...         yield n
...         n -= 1
...     print('Done!')
...

>>> # Create the generator, notice no output appears
>>> c = countdown(3)
>>> c
<generator object countdown at 0x1006a0af0>

>>> # Run to first yield and emit a value
>>> next(c)
Starting to count from 3
3

>>> # Run to the next yield
>>> next(c)
2

>>> # Run to next yield
>>> next(c)
1

>>> # Run to next yield (iteration stops)
>>> next(c)
Done!
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration
>>>

一個(gè)生成器函數(shù)主要特征是它只會(huì)回應(yīng)在迭代中使用到的 next 操作。 一旦生成器函數(shù)返回退出,迭代終止。我們在迭代中通常使用的 for 語句會(huì)自動(dòng)處理這些細(xì)節(jié),所以你無需擔(dān)心。

實(shí)現(xiàn)迭代器協(xié)議

問題

你想構(gòu)建一個(gè)能支持迭代操作的自定義對象,并希望找到一個(gè)能實(shí)現(xiàn)迭代協(xié)議的簡單方法。

解決方案

目前為止,在一個(gè)對象上實(shí)現(xiàn)迭代最簡單的方式是使用一個(gè)生成器函數(shù)。 在4.2小節(jié)中,使用 Node 類來表示樹形數(shù)據(jù)結(jié)構(gòu)。你可能想實(shí)現(xiàn)一個(gè)以深度優(yōu)先方式遍歷樹形節(jié)點(diǎn)的生成器。 下面是代碼示例:

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:
            yield from c.depth_first()

# Example
if __name__ == '__main__':
    root = Node(0)
    child1 = Node(1)
    child2 = Node(2)
    root.add_child(child1)
    root.add_child(child2)
    child1.add_child(Node(3))
    child1.add_child(Node(4))
    child2.add_child(Node(5))

    for ch in root.depth_first():
        print(ch)
    # Outputs Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)

在這段代碼中,depth_first()方法簡單直觀。 它首先返回自己本身并迭代每一個(gè)子節(jié)點(diǎn)并 通過調(diào)用子節(jié)點(diǎn)的 depth_first() 方法(使用yield from語句)返回對應(yīng)元素。

討論

Python 的迭代協(xié)議要求一個(gè) __iter__() 方法返回一個(gè)特殊的迭代器對象, 這個(gè)迭代器對象實(shí)現(xiàn)了 __next__() 方法并通過 StopIteration 異常標(biāo)識迭代的完成。 但是,實(shí)現(xiàn)這些通常會(huì)比較繁瑣。 下面我們演示下這種方式,如何使用一個(gè)關(guān)聯(lián)迭代器類重新實(shí)現(xiàn) depth_first() 方法:

class Node2:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        return DepthFirstIterator(self)

class DepthFirstIterator(object):
    '''
    Depth-first traversal
    '''

    def __init__(self, start_node):
        self._node = start_node
        self._children_iter = None
        self._child_iter = None

    def __iter__(self):
        return self

    def __next__(self):
        # Return myself if just started; create an iterator for children
        if self._children_iter is None:
            self._children_iter = iter(self._node)
            return self._node
        # If processing a child, return its next item
        elif self._child_iter:
            try:
                nextchild = next(self._child_iter)
                return nextchild
            except StopIteration:
                self._child_iter = None
                return next(self)
        # Advance to the next child and start its iteration
        else:
            self._child_iter = next(self._children_iter).depth_first()
            return next(self)

DepthFirstIterator類和上面使用生成器的版本工作原理類似, 但是它寫起來很繁瑣,因?yàn)榈鞅仨氃诘幚磉^程中維護(hù)大量的狀態(tài)信息。 坦白來講,沒人愿意寫這么晦澀的代碼。將你的迭代器定義為一個(gè)生成器后一切迎刃而解。

反向迭代

問題

你想反方向迭代一個(gè)序列

解決方案

使用內(nèi)置的 reversed()函數(shù),比如:

>>> a = [1, 2, 3, 4]
>>> for x in reversed(a):
...     print(x)
...
4
3
2
1

反向迭代僅僅當(dāng)對象的大小可預(yù)先確定或者對象實(shí)現(xiàn)了 __reversed__() 的特殊方法時(shí)才能生效。 如果兩者都不符合,那你必須先將對象轉(zhuǎn)換為一個(gè)列表才行,比如:

# Print a file backwards
f = open('somefile')
for line in reversed(list(f)):
    print(line, end='')

要注意的是如果可迭代對象元素很多的話,將其預(yù)先轉(zhuǎn)換為一個(gè)列表要消耗大量的內(nèi)存。

討論

很多程序員并不知道可以通過在自定義類上實(shí)現(xiàn)__reversed__() 方法來實(shí)現(xiàn)反向迭代。比如:

class Countdown:
    def __init__(self, start):
        self.start = start

    # Forward iterator
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

    # Reverse iterator
    def __reversed__(self):
        n = 1
        while n <= self.start:
            yield n
            n += 1

for rr in reversed(Countdown(30)):
    print(rr)
for rr in Countdown(30):
    print(rr)

定義一個(gè)反向迭代器可以使得代碼非常的高效, 因?yàn)樗辉傩枰獙?shù)據(jù)填充到一個(gè)列表中然后再去反向迭代這個(gè)列表。

帶有外部狀態(tài)的生成器函數(shù)

問題

你想定義一個(gè)生成器函數(shù),但是它會(huì)調(diào)用某個(gè)你想暴露給用戶使用的外部狀態(tài)值。

解決方案

如果你想讓你的生成器暴露外部狀態(tài)給用戶, 別忘了你可以簡單的將它實(shí)現(xiàn)為一個(gè)類,然后把生成器函數(shù)放到 __iter__() 方法中過去。比如:

from collections import deque

class linehistory:
    def __init__(self, lines, histlen=3):
        self.lines = lines
        self.history = deque(maxlen=histlen)

    def __iter__(self):
        for lineno, line in enumerate(self.lines, 1):
            self.history.append((lineno, line))
            yield line

    def clear(self):
        self.history.clear()

為了使用這個(gè)類,你可以將它當(dāng)做是一個(gè)普通的生成器函數(shù)。 然而,由于可以創(chuàng)建一個(gè)實(shí)例對象,于是你可以訪問內(nèi)部屬性值, 比如 history 屬性或者是 clear() 方法。代碼示例如下:

with open('somefile.txt') as f:
    lines = linehistory(f)
    for line in lines:
        if 'python' in line:
            for lineno, hline in lines.history:
                print('{}:{}'.format(lineno, hline), end='')

討論

關(guān)于生成器,很容易掉進(jìn)函數(shù)無所不能的陷阱。 如果生成器函數(shù)需要跟你的程序其他部分打交道的話(比如暴露屬性值,允許通過方法調(diào)用來控制等等), 可能會(huì)導(dǎo)致你的代碼異常的復(fù)雜。 如果是這種情況的話,可以考慮使用上面介紹的定義類的方式。 在 __iter__()方法中定義你的生成器不會(huì)改變你任何的算法邏輯。 由于它是類的一部分,所以允許你定義各種屬性和方法來供用戶使用。

一個(gè)需要注意的小地方是,如果你在迭代操作時(shí)不使用 for 循環(huán)語句,那么你得先調(diào)用 iter()函數(shù)。比如:

>>> f = open('somefile.txt')
>>> lines = linehistory(f)
>>> next(lines)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: 'linehistory' object is not an iterator

>>> # Call iter() first, then start iterating
>>> it = iter(lines)
>>> next(it)
'hello world\n'
>>> next(it)
'this is a test\n'
>>>

迭代器切片

問題

你想得到一個(gè)由迭代器生成的切片對象,但是標(biāo)準(zhǔn)切片操作并不能做到。

解決方案

函數(shù) itertools.islice()正好適用于在迭代器和生成器上做切片操作。比如:

>>> def count(n):
...     while True:
...         yield n
...         n += 1
...
>>> c = count(0)
>>> c[10:20]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20):
...     print(x)
...
10
11
12
13
14
15
16
17
18
19
>>>

討論

迭代器和生成器不能使用標(biāo)準(zhǔn)的切片操作,因?yàn)樗鼈兊拈L度事先我們并不知道(并且也沒有實(shí)現(xiàn)索引)。 函數(shù) islice()返回一個(gè)可以生成指定元素的迭代器,它通過遍歷并丟棄直到切片開始索引位置的所有元素。 然后才開始一個(gè)個(gè)的返回元素,并直到切片結(jié)束索引位置。

這里要著重強(qiáng)調(diào)的一點(diǎn)是 islice() 會(huì)消耗掉傳入的迭代器中的數(shù)據(jù)。 必須考慮到迭代器是不可逆的這個(gè)事實(shí)。 所以如果你需要之后再次訪問這個(gè)迭代器的話,那你就得先將它里面的數(shù)據(jù)放入一個(gè)列表中。

跳過可迭代對象的開始部分

問題

你想遍歷一個(gè)可迭代對象,但是它開始的某些元素你并不感興趣,想跳過它們。

解決方案

itertools 模塊中有一些函數(shù)可以完成這個(gè)任務(wù)。 首先介紹的是 itertools.dropwhile()函數(shù)。使用時(shí),你給它傳遞一個(gè)函數(shù)對象和一個(gè)可迭代對象。 它會(huì)返回一個(gè)迭代器對象,丟棄原有序列中直到函數(shù)返回 True 之前的所有元素,然后返回后面所有元素。

為了演示,假定你在讀取一個(gè)開始部分是幾行注釋的源文件。比如:

>>> with open('/etc/passwd') as f:
... for line in f:
...     print(line, end='')
...
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode. At other times, this information is provided by
# Open Directory.
...
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>

如果你想跳過開始部分的注釋行的話,可以這樣做:

>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
...     for line in dropwhile(lambda line: line.startswith('#'), f):
...         print(line, end='')
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>

這個(gè)例子是基于根據(jù)某個(gè)測試函數(shù)跳過開始的元素。 如果你已經(jīng)明確知道了要跳過的元素的個(gè)數(shù)的話,那么可以使用 itertools.islice() 來代替。比如:

>>> from itertools import islice
>>> items = ['a', 'b', 'c', 1, 4, 10, 15]
>>> for x in islice(items, 3, None):
...     print(x)
...
1
4
10
15
>>>

在這個(gè)例子中, islice() 函數(shù)最后那個(gè) None 參數(shù)指定了你要獲取從第3個(gè)到最后的所有元素, 如果 None 和3的位置對調(diào),意思就是僅僅獲取前三個(gè)元素恰恰相反, (這個(gè)跟切片的相反操作 [3:][:3]原理是一樣的)。

討論

函數(shù) dropwhile()islice() 其實(shí)就是兩個(gè)幫助函數(shù),為的就是避免寫出下面這種冗余代碼:

with open('/etc/passwd') as f:
    # Skip over initial comments
    while True:
        line = next(f, '')
        if not line.startswith('#'):
            break

    # Process remaining lines
    while line:
        # Replace with useful processing
        print(line, end='')
        line = next(f, None)

跳過一個(gè)可迭代對象的開始部分跟通常的過濾是不同的。 比如,上述代碼的第一個(gè)部分可能會(huì)這樣重寫:

with open('/etc/passwd') as f:
    lines = (line for line in f if not line.startswith('#'))
    for line in lines:
        print(line, end='')

這樣寫確實(shí)可以跳過開始部分的注釋行,但是同樣也會(huì)跳過文件中其他所有的注釋行。 換句話講,我們的解決方案是僅僅跳過開始部分滿足測試條件的行,在那以后,所有的元素不再進(jìn)行測試和過濾了。

最后需要著重強(qiáng)調(diào)的一點(diǎn)是,本節(jié)的方案適用于所有可迭代對象,包括那些事先不能確定大小的, 比如生成器,文件及其類似的對象。

排列組合的迭代

問題

你想迭代遍歷一個(gè)集合中元素的所有可能的排列或組合

解決方案

itertools 模塊提供了三個(gè)函數(shù)來解決這類問題。 其中一個(gè)是 itertools.permutations(), 它接受一個(gè)集合并產(chǎn)生一個(gè)元組序列,每個(gè)元組由集合中所有元素的一個(gè)可能排列組成。 也就是說通過打亂集合中元素排列順序生成一個(gè)元組,比如:

>>> items = ['a', 'b', 'c']
>>> from itertools import permutations
>>> for p in permutations(items):
...     print(p)
...
('a', 'b', 'c')
('a', 'c', 'b')
('b', 'a', 'c')
('b', 'c', 'a')
('c', 'a', 'b')
('c', 'b', 'a')
>>>

如果你想得到指定長度的所有排列,你可以傳遞一個(gè)可選的長度參數(shù)。就像這樣:

>>> for p in permutations(items, 2):
...     print(p)
...
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'c')
('c', 'a')
('c', 'b')
>>>

使用 itertools.combinations()可得到輸入集合中元素的所有的組合。比如:

>>> from itertools import combinations
>>> for c in combinations(items, 3):
...     print(c)
...
('a', 'b', 'c')

>>> for c in combinations(items, 2):
...     print(c)
...
('a', 'b')
('a', 'c')
('b', 'c')

>>> for c in combinations(items, 1):
...     print(c)
...
('a',)
('b',)
('c',)
>>>

對于 combinations() 來講,元素的順序已經(jīng)不重要了。 也就是說,組合 ('a', 'b')('b', 'a')其實(shí)是一樣的(最終只會(huì)輸出其中一個(gè))。

在計(jì)算組合的時(shí)候,一旦元素被選取就會(huì)從候選中剔除掉(比如如果元素’a’已經(jīng)被選取了,那么接下來就不會(huì)再考慮它了)。 而函數(shù) itertools.combinations_with_replacement()允許同一個(gè)元素被選擇多次,比如:

>>> for c in combinations_with_replacement(items, 3):
...     print(c)
...
('a', 'a', 'a')
('a', 'a', 'b')
('a', 'a', 'c')
('a', 'b', 'b')
('a', 'b', 'c')
('a', 'c', 'c')
('b', 'b', 'b')
('b', 'b', 'c')
('b', 'c', 'c')
('c', 'c', 'c')
>>>

討論

這一小節(jié)我們向你展示的僅僅是 itertools 模塊的一部分功能。 盡管你也可以自己手動(dòng)實(shí)現(xiàn)排列組合算法,但是這樣做得要花點(diǎn)腦力。 當(dāng)我們碰到看上去有些復(fù)雜的迭代問題時(shí),最好可以先去看看 itertools 模塊。 如果這個(gè)問題很普遍,那么很有可能會(huì)在里面找到解決方案!

序列上索引值迭代

問題

你想在迭代一個(gè)序列的同時(shí)跟蹤正在被處理的元素索引。

解決方案

內(nèi)置的 enumerate() 函數(shù)可以很好的解決這個(gè)問題:

>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list):
...     print(idx, val)
...
0 a
1 b
2 c

為了按傳統(tǒng)行號輸出(行號從1開始),你可以傳遞一個(gè)開始參數(shù):

>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list, 1):
...     print(idx, val)
...
1 a
2 b
3 c

這種情況在你遍歷文件時(shí)想在錯(cuò)誤消息中使用行號定位時(shí)候非常有用:

def parse_data(filename):
    with open(filename, 'rt') as f:
        for lineno, line in enumerate(f, 1):
            fields = line.split()
            try:
                count = int(fields[1])
                ...
            except ValueError as e:
                print('Line {}: Parse error: {}'.format(lineno, e))

enumerate() 對于跟蹤某些值在列表中出現(xiàn)的位置是很有用的。 所以,如果你想將一個(gè)文件中出現(xiàn)的單詞映射到它出現(xiàn)的行號上去,可以很容易的利用 enumerate()來完成:

word_summary = defaultdict(list)

with open('myfile.txt', 'r') as f:
    lines = f.readlines()

for idx, line in enumerate(lines):
    # Create a list of words in current line
    words = [w.strip().lower() for w in line.split()]
    for word in words:
        word_summary[word].append(idx)

如果你處理完文件后打印 word_summary,會(huì)發(fā)現(xiàn)它是一個(gè)字典(準(zhǔn)確來講是一個(gè) defaultdict ), 對于每個(gè)單詞有一個(gè) key ,每個(gè) key 對應(yīng)的值是一個(gè)由這個(gè)單詞出現(xiàn)的行號組成的列表。 如果某個(gè)單詞在一行中出現(xiàn)過兩次,那么這個(gè)行號也會(huì)出現(xiàn)兩次, 同時(shí)也可以作為文本的一個(gè)簡單統(tǒng)計(jì)。

討論

當(dāng)你想額外定義一個(gè)計(jì)數(shù)變量的時(shí)候,使用 enumerate() 函數(shù)會(huì)更加簡單。你可能會(huì)像下面這樣寫代碼:

lineno = 1
for line in f:
    # Process line
    ...
    lineno += 1

但是如果使用 enumerate() 函數(shù)來代替就顯得更加優(yōu)雅了:

for lineno, line in enumerate(f):
    # Process line
    ...

enumerate()函數(shù)返回的是一個(gè) enumerate對象實(shí)例, 它是一個(gè)迭代器,返回連續(xù)的包含一個(gè)計(jì)數(shù)和一個(gè)值的元組, 元組中的值通過在傳入序列上調(diào)用 next()返回。

還有一點(diǎn)可能并不很重要,但是也值得注意, 有時(shí)候當(dāng)你在一個(gè)已經(jīng)解壓后的元組序列上使用 enumerate() 函數(shù)時(shí)很容易調(diào)入陷阱。 你得像下面正確的方式這樣寫:

data = [ (1, 2), (3, 4), (5, 6), (7, 8) ]

# Correct!
for n, (x, y) in enumerate(data):
    ...
# Error!
for n, x, y in enumerate(data):
    ...

同時(shí)迭代多個(gè)序列

問題

你想同時(shí)迭代多個(gè)序列,每次分別從一個(gè)序列中取一個(gè)元素。

解決方案

為了同時(shí)迭代多個(gè)序列,使用 zip() 函數(shù)。比如:

>>> xpts = [1, 5, 4, 2, 10, 7]
>>> ypts = [101, 78, 37, 15, 62, 99]
>>> for x, y in zip(xpts, ypts):
...     print(x,y)
...
1 101
5 78
4 37
2 15
10 62
7 99
>>>

zip(a, b)會(huì)生成一個(gè)可返回元組 (x, y) 的迭代器,其中 x 來自 a,y 來自 b。 一旦其中某個(gè)序列到底結(jié)尾,迭代宣告結(jié)束。 因此迭代長度跟參數(shù)中最短序列長度一致。

>>> a = [1, 2, 3]
>>> b = ['w', 'x', 'y', 'z']
>>> for i in zip(a,b):
...     print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
>>>

如果這個(gè)不是你想要的效果,那么還可以使用 itertools.zip_longest() 函數(shù)來代替。比如:

>>> from itertools import zip_longest
>>> for i in zip_longest(a,b):
...     print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
(None, 'z')

>>> for i in zip_longest(a, b, fillvalue=0):
...     print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
(0, 'z')
>>>

討論

當(dāng)你想成對處理數(shù)據(jù)的時(shí)候 zip() 函數(shù)是很有用的。 比如,假設(shè)你頭列表和一個(gè)值列表,就像下面這樣:

headers = ['name', 'shares', 'price']
values = ['ACME', 100, 490.1]

使用 zip() 可以讓你將它們打包并生成一個(gè)字典:

s = dict(zip(headers,values))

或者你也可以像下面這樣產(chǎn)生輸出:

for name, val in zip(headers, values):
    print(name, '=', val)

雖然不常見,但是 zip()可以接受多于兩個(gè)的序列的參數(shù)。 這時(shí)候所生成的結(jié)果元組中元素個(gè)數(shù)跟輸入序列個(gè)數(shù)一樣。比如;

>>> a = [1, 2, 3]
>>> b = [10, 11, 12]
>>> c = ['x','y','z']
>>> for i in zip(a, b, c):
...     print(i)
...
(1, 10, 'x')
(2, 11, 'y')
(3, 12, 'z')
>>>

最后強(qiáng)調(diào)一點(diǎn)就是,zip() 會(huì)創(chuàng)建一個(gè)迭代器來作為結(jié)果返回。 如果你需要將結(jié)對的值存儲在列表中,要使用list() 函數(shù)。比如:

>>> zip(a, b)
<zip object at 0x1007001b8>
>>> list(zip(a, b))
[(1, 10), (2, 11), (3, 12)]
>>>

不同集合上元素的迭代

問題

你想在多個(gè)對象執(zhí)行相同的操作,但是這些對象在不同的容器中,你希望代碼在不失可讀性的情況下避免寫重復(fù)的循環(huán)。

解決方案

itertools.chain() 方法可以用來簡化這個(gè)任務(wù)。 它接受一個(gè)可迭代對象列表作為輸入,并返回一個(gè)迭代器,有效的屏蔽掉在多個(gè)容器中迭代細(xì)節(jié)。 為了演示清楚,考慮下面這個(gè)例子:

>>> from itertools import chain
>>> a = [1, 2, 3, 4]
>>> b = ['x', 'y', 'z']
>>> for x in chain(a, b):
... print(x)
...
1
2
3
4
x
y
z
>>>

使用 chain() 的一個(gè)常見場景是當(dāng)你想對不同的集合中所有元素執(zhí)行某些操作的時(shí)候。比如:

# Various working sets of items
active_items = set()
inactive_items = set()

# Iterate over all items
for item in chain(active_items, inactive_items):
    # Process item

這種解決方案要比像下面這樣使用兩個(gè)單獨(dú)的循環(huán)更加優(yōu)雅,

for item in active_items:
    # Process item
    ...

for item in inactive_items:
    # Process item
    ...

討論

itertools.chain() 接受一個(gè)或多個(gè)可迭代對象最為輸入?yún)?shù)。 然后創(chuàng)建一個(gè)迭代器,依次連續(xù)的返回每個(gè)可迭代對象中的元素。 這種方式要比先將序列合并再迭代要高效的多。比如:

# Inefficent
for x in a + b:
    ...

# Better
for x in chain(a, b):
    ...

第一種方案中,a + b操作會(huì)創(chuàng)建一個(gè)全新的序列并要求a和b的類型一致。 chian() 不會(huì)有這一步,所以如果輸入序列非常大的時(shí)候會(huì)很省內(nèi)存。 并且當(dāng)可迭代對象類型不一樣的時(shí)候 chain() 同樣可以很好的工作。

創(chuàng)建數(shù)據(jù)處理管道

問題

你想以數(shù)據(jù)管道(類似 Unix 管道)的方式迭代處理數(shù)據(jù)。 比如,你有個(gè)大量的數(shù)據(jù)需要處理,但是不能將它們一次性放入內(nèi)存中。

解決方案

生成器函數(shù)是一個(gè)實(shí)現(xiàn)管道機(jī)制的好辦法。 為了演示,假定你要處理一個(gè)非常大的日志文件目錄:

foo/
    access-log-012007.gz
    access-log-022007.gz
    access-log-032007.gz
    ...
    access-log-012008
bar/
    access-log-092007.bz2
    ...
    access-log-022008

假設(shè)每個(gè)日志文件包含這樣的數(shù)據(jù):

124.115.6.12 - - [10/Jul/2012:00:18:50 -0500] "GET /robots.txt ..." 200 71
210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /ply/ ..." 200 11875
210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /favicon.ico ..." 404 369
61.135.216.105 - - [10/Jul/2012:00:20:04 -0500] "GET /blog/atom.xml ..." 304 -
...

為了處理這些文件,你可以定義一個(gè)由多個(gè)執(zhí)行特定任務(wù)獨(dú)立任務(wù)的簡單生成器函數(shù)組成的容器。就像這樣:

import os
import fnmatch
import gzip
import bz2
import re

def gen_find(filepat, top):
    '''
    Find all filenames in a directory tree that match a shell wildcard pattern
    '''
    for path, dirlist, filelist in os.walk(top):
        for name in fnmatch.filter(filelist, filepat):
            yield os.path.join(path,name)

def gen_opener(filenames):
    '''
    Open a sequence of filenames one at a time producing a file object.
    The file is closed immediately when proceeding to the next iteration.
    '''
    for filename in filenames:
        if filename.endswith('.gz'):
            f = gzip.open(filename, 'rt')
        elif filename.endswith('.bz2'):
            f = bz2.open(filename, 'rt')
        else:
            f = open(filename, 'rt')
        yield f
        f.close()

def gen_concatenate(iterators):
    '''
    Chain a sequence of iterators together into a single sequence.
    '''
    for it in iterators:
        yield from it

def gen_grep(pattern, lines):
    '''
    Look for a regex pattern in a sequence of lines
    '''
    pat = re.compile(pattern)
    for line in lines:
        if pat.search(line):
            yield line

現(xiàn)在你可以很容易的將這些函數(shù)連起來創(chuàng)建一個(gè)處理管道。 比如,為了查找包含單詞 python 的所有日志行,你可以這樣做:

lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
for line in pylines:
    print(line)

如果將來的時(shí)候你想擴(kuò)展管道,你甚至可以在生成器表達(dá)式中包裝數(shù)據(jù)。 比如,下面這個(gè)版本計(jì)算出傳輸?shù)淖止?jié)數(shù)并計(jì)算其總和。

lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
bytecolumn = (line.rsplit(None,1)[1] for line in pylines)
bytes = (int(x) for x in bytecolumn if x != '-')
print('Total', sum(bytes))

討論

以管道方式處理數(shù)據(jù)可以用來解決各類其他問題,包括解析,讀取實(shí)時(shí)數(shù)據(jù),定時(shí)輪詢等。

為了理解上述代碼,重點(diǎn)是要明白 yield 語句作為數(shù)據(jù)的生產(chǎn)者而 for 循環(huán)語句作為數(shù)據(jù)的消費(fèi)者。 當(dāng)這些生成器被連在一起后,每個(gè) yield 會(huì)將一個(gè)單獨(dú)的數(shù)據(jù)元素傳遞給迭代處理管道的下一階段。 在例子最后部分,sum() 函數(shù)是最終的程序驅(qū)動(dòng)者,每次從生成器管道中提取出一個(gè)元素。

這種方式一個(gè)非常好的特點(diǎn)是每個(gè)生成器函數(shù)很小并且都是獨(dú)立的。這樣的話就很容易編寫和維護(hù)它們了。 很多時(shí)候,這些函數(shù)如果比較通用的話可以在其他場景重復(fù)使用。 并且最終將這些組件組合起來的代碼看上去非常簡單,也很容易理解。

使用這種方式的內(nèi)存效率也不得不提。上述代碼即便是在一個(gè)超大型文件目錄中也能工作的很好。 事實(shí)上,由于使用了迭代方式處理,代碼運(yùn)行過程中只需要很小很小的內(nèi)存。

在調(diào)用 gen_concatenate() 函數(shù)的時(shí)候你可能會(huì)有些不太明白。 這個(gè)函數(shù)的目的是將輸入序列拼接成一個(gè)很長的行序列。itertools.chain()函數(shù)同樣有類似的功能,但是它需要將所有可迭代對象最為參數(shù)傳入。 在上面這個(gè)例子中,你可能會(huì)寫類似這樣的語句lines = itertools.chain(*files) , 使得gen_opener()生成器能被全部消費(fèi)掉。 但由于 gen_opener()生成器每次生成一個(gè)打開過的文件, 等到下一個(gè)迭代步驟時(shí)文件就關(guān)閉了,因此 china() 在這里不能這樣使用。 上面的方案可以避免這種情況。

gen_concatenate() 函數(shù)中出現(xiàn)過 yield from語句,它將 yield操作代理到父生成器上去。 語句 yield from it 簡單的返回生成器 it所產(chǎn)生的所有值。 關(guān)于這個(gè)我們在4.14小節(jié)會(huì)有更進(jìn)一步的描述。

最后還有一點(diǎn)需要注意的是,管道方式并不是萬能的。 有時(shí)候你想立即處理所有數(shù)據(jù)。 然而,即便是這種情況,使用生成器管道也可以將這類問題從邏輯上變?yōu)楣ぷ髁鞯奶幚矸绞健?/p>

David Beazley 在他的 Generator Tricks for Systems Programmers 教程中對于這種技術(shù)有非常深入的講解。可以參考這個(gè)教程獲取更多的信息。

展開嵌套的序列

問題

你想將一個(gè)多層嵌套的序列展開成一個(gè)單層列表

解決方案

可以寫一個(gè)包含 yield from 語句的遞歸生成器來輕松解決這個(gè)問題。比如:

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            yield from flatten(x)
        else:
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]
# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
    print(x)

在上面代碼中, isinstance(x, Iterable) 檢查某個(gè)元素是否是可迭代的。 如果是的話, yield from 就會(huì)返回所有子例程的值。最終返回結(jié)果就是一個(gè)沒有嵌套的簡單序列了。

額外的參數(shù) ignore_types 和檢測語句 isinstance(x, ignore_types) 用來將字符串和字節(jié)排除在可迭代對象外,防止將它們再展開成單個(gè)的字符。 這樣的話字符串?dāng)?shù)組就能最終返回我們所期望的結(jié)果了。比如:

>>> items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
>>> for x in flatten(items):
...     print(x)
...
Dave
Paula
Thomas
Lewis
>>>

討論

語句 yield from 在你想在生成器中調(diào)用其他生成器作為子例程的時(shí)候非常有用。 如果你不使用它的話,那么就必須寫額外的for 循環(huán)了。比如:

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            for i in flatten(x):
                yield i
        else:
            yield x

盡管只改了一點(diǎn)點(diǎn),但是 yield from 語句看上去感覺更好,并且也使得代碼更簡潔清爽。

之前提到的對于字符串和字節(jié)的額外檢查是為了防止將它們再展開成單個(gè)字符。 如果還有其他你不想展開的類型,修改參數(shù) ignore_types 即可。

最后要注意的一點(diǎn)是,yield from 在涉及到基于協(xié)程和生成器的并發(fā)編程中扮演著更加重要的角色。 可以參考12.12小節(jié)查看另外一個(gè)例子。

順序迭代合并后的排序迭代對象

問題

你有一系列排序序列,想將它們合并后得到一個(gè)排序序列并在上面迭代遍歷。

解決方案

heapq.merge() 函數(shù)可以幫你解決這個(gè)問題。比如:

>>> import heapq
>>> a = [1, 4, 7, 10]
>>> b = [2, 5, 6, 11]
>>> for c in heapq.merge(a, b):
...     print(c)
...
1
2
4
5
6
7
10
11

討論

heapq.merge 可迭代特性意味著它不會(huì)立馬讀取所有序列。 這就意味著你可以在非常長的序列中使用它,而不會(huì)有太大的開銷。 比如,下面是一個(gè)例子來演示如何合并兩個(gè)排序文件:

with open('sorted_file_1', 'rt') as file1, \
    open('sorted_file_2', 'rt') as file2, \
    open('merged_file', 'wt') as outf:

    for line in heapq.merge(file1, file2):
        outf.write(line)

有一點(diǎn)要強(qiáng)調(diào)的是 heapq.merge()需要所有輸入序列必須是排過序的。 特別的,它并不會(huì)預(yù)先讀取所有數(shù)據(jù)到堆棧中或者預(yù)先排序,也不會(huì)對輸入做任何的排序檢測。 它僅僅是檢查所有序列的開始部分并返回最小的那個(gè),這個(gè)過程一直會(huì)持續(xù)直到所有輸入序列中的元素都被遍歷完。

迭代器代替 while 無限循環(huán)

問題

你在代碼中使用while循環(huán)來迭代處理數(shù)據(jù),因?yàn)樗枰{(diào)用某個(gè)函數(shù)或者和一般迭代模式不同的測試條件。 能不能用迭代器來重寫這個(gè)循環(huán)呢?

解決方案

一個(gè)常見的 IO 操作程序可能會(huì)想下面這樣:

CHUNKSIZE = 8192

def reader(s):
    while True:
        data = s.recv(CHUNKSIZE)
        if data == b'':
            break
        process_data(data)

這種代碼通??梢允褂?iter()來代替,如下所示:

def reader2(s):
    for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
        pass
        # process_data(data)

如果你懷疑它到底能不能正常工作,可以試驗(yàn)下一個(gè)簡單的例子。比如:

>>> import sys
>>> f = open('/etc/passwd')
>>> for chunk in iter(lambda: f.read(10), ''):
...     n = sys.stdout.write(chunk)
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
_uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico
...
>>>

討論

iter 函數(shù)一個(gè)鮮為人知的特性是它接受一個(gè)可選的 callable 對象和一個(gè)標(biāo)記(結(jié)尾)值作為輸入?yún)?shù)。 當(dāng)以這種方式使用的時(shí)候,它會(huì)創(chuàng)建一個(gè)迭代器, 這個(gè)迭代器會(huì)不斷調(diào)用 callable對象直到返回值和標(biāo)記值相等為止。

這種特殊的方法對于一些特定的會(huì)被重復(fù)調(diào)用的函數(shù)很有效果,比如涉及到 I/O 調(diào)用的函數(shù)。 舉例來講,如果你想從套接字或文件中以數(shù)據(jù)塊的方式讀取數(shù)據(jù),通常你得要不斷重復(fù)的執(zhí)行 read()recv() , 并在后面緊跟一個(gè)文件結(jié)尾測試來決定是否終止。這節(jié)中的方案使用一個(gè)簡單的iter()調(diào)用就可以將兩者結(jié)合起來了。 其中 lambda函數(shù)參數(shù)是為了創(chuàng)建一個(gè)無參的 callable對象,并為 recvread()方法提供了 size參數(shù)。