更深入的理解 Python 中的迭代

從我們作為 Python 程序員的角度來看,你可以使用迭代器來做的唯一有用的事情是將其傳遞給內置的 next 函數,或者對其進行循環遍歷:

>>> next(iterator)

1

>>> list(iterator)

[2, 3, 5, 7]

如果我們第二次循環遍歷迭代器,我們將一無所獲:

>>> list(iterator)

[]

你可以把迭代器看作是惰性迭代器,它們是一次性使用,這意味著它們只能循環遍歷一次。

正如你在下面的真值表中所看到的,可迭代對象並不總是迭代器,但是迭代器總是可迭代的:

對象可迭代?迭代器?
可迭代對象V?
迭代器VV
生成器VV
列表VX

全部的迭代器協議

讓我們從 Python 的角度來定義迭代器是如何工作的。

可迭代對象可以被傳遞給 iter 函數,以便為它們獲得迭代器。

迭代器:

  • 可以傳遞給 next 函數,它將給出下一項,如果沒有下一項,那麼它將會引發 StopIteration 異常

  • 可以傳遞給 iter 函數,它會返回一個自身的迭代器

這些語句反過來也是正確的:

  • 任何可以在不引發 TypeError 異常的情況下傳遞給 iter 的東西都是可迭代的

  • 任何可以在不引發 TypeError 異常的情況下傳遞給 next 的東西都是一個迭代器

  • 當傳遞給 iter 時,任何返回自身的東西都是一個迭代器

這就是 Python 中的迭代器協議。

迭代器的惰性

迭代器允許我們一起工作,創建惰性可迭代對象,即在我們要求它們提供下一項之前,它們不做任何事情。因為可以創建惰性迭代器,所以我們可以創建無限長的迭代器。我們可以創建對系統資源比較保守的迭代器,可以節省我們的內存,節省 CPU 時間。

迭代器無處不在

你已經在 Python 中看到過許多迭代器,我也提到過生成器是迭代器。Python 的許多內置類型也是迭代器。例如,Python 的 enumerate 和 reversed 對象就是迭代器。

>>> letters = ['a', 'b', 'c']

>>> e = enumerate(letters)

>>> e

>>> next(e)

(0, 'a')

在 Python 3 中,zip, map 和 filter 也是迭代器。

>>> numbers = [1, 2, 3, 5, 7]

>>> letters = ['a', 'b', 'c']

>>> z = zip(numbers, letters)

>>> z

>>> next(z)

(1, 'a')

Python 中的文件對象也是迭代器。

>>> next(open('hello.txt'))

'hello world\n'

在 Python 標準庫和第三方庫中內置了大量的迭代器。這些迭代器首先惰性迭代器一樣,延遲工作直到你請求它們下一項。

創建你自己的迭代器

知道你已經在使用迭代器是很有用的,但是我希望你也知道,你可以創建自己的迭代器和你自己的惰性迭代器。

下面這個類構造了一個迭代器接受一個可迭代的數字,並在循環結束時提供每個數字的平方。

class square_all:

def __init__(self, numbers):

self.numbers = iter(numbers)

def __next__(self):

return next(self.numbers) * 2

def __iter__(self):

return self

但是在我們開始對該類的實例進行循環遍歷之前,沒有任何工作要做。

這裡,我們有一個無限長的可迭代對象 count,你可以看到 square_all 接受 count 而不用完全循環遍歷這個無限長的迭代:

>>> from itertools import count

>>> numbers = count(5)

>>> squares = square_all(numbers)

>>> next(squares)

25

>>> next(squares)

36

這個迭代器類是有效的,但我們通常不會這樣做。通常,當我們想要做一個定製的迭代器時,我們會生成一個生成器函數:

def square_all(numbers):

for n in numbers:

yield n**2

這個生成器函數等價於我們上面所做的類,它的工作原理是一樣的。

這種 yield 語句似乎很神奇,但它非常強大:yield 允許我們在調用 next 函數之間暫停生成器函數。yield 語句是將生成器函數與常規函數分離的東西。

另一種實現相同迭代器的方法是使用生成器表達式。

def square_all(numbers):

return (n**2 for n in numbers)

這和我們的生成器函數確實是一樣的,但是它使用的語法看起來 像是一個列表推導一樣 。如果你需要在代碼中使用惰性迭代,請考慮迭代器,並考慮使用生成器函數或生成器表達式。

迭代器如何改進你的代碼

一旦你已經接受了在代碼中使用惰性迭代器的想法,你就會發現有很多可能來發現或創建輔助函數,以此來幫助你循環遍歷和處理數據。

惰性求和

這是一個 for 循環,它對 Django queryset 中的所有工作時間求和:

hours_worked = 0

for event in events:

if event.is_billable():

hours_worked += event.duration

下面是使用生成器表達式進行惰性評估的代碼:

billable_times = (

event.duration

for event in events

if event.is_billable()

)

hours_worked = sum(billable_times)

請注意,我們代碼的形狀發生了巨大變化。

將我們的計算工作時間變成一個惰性迭代器允許我們能夠命名以前未命名(billable_times)的東西。這也允許我們使用 sum 函數,我們以前不能使用 sum 函數是因為我們甚至沒有一個可迭代對象傳遞給它。迭代器允許你從根本上改變你組織代碼的方式。

惰性和打破循環

這段代碼打印出日誌文件的前 10 行:

for i, line in enumerate(log_file):

if i >= 10:

break

print(line)

這段代碼做了同樣的事情,但是我們使用的是 itertools.islice 函數來惰性地抓取文件中的前 10 行:

from itertools import islice

first_ten_lines = islice(log_file, 10)

for line in first_ten_lines:

print(line)

我們定義的 first_ten_lines 變量是迭代器,同樣,使用迭代器允許我們給以前未命名的東西命名(first_ten_lines)。命名事物可以使我們的代碼更具描述性,更具可讀性。

作為獎勵,我們還消除了在循環中使用 break 語句的需要,因為 islice 實用函數為我們處理了中斷。

你可以在標準庫中的 itertools 中找到更多的迭代輔助函數,以及諸如 boltons 和 more-itertools 之類的第三方庫。

創建自己的迭代輔助函數

你可以在標準庫和第三方庫中找到用於循環的輔助函數,但你也可以自己創建!

這段代碼列出了序列中連續值之間的差值列表。

current = readings[0]

for next_item in readings[1:]:

differences.append(next_item - current)

current = next_item

請注意,這段代碼中有一個額外的變量,我們每次循環時都要指定它。還要注意,這段代碼只適用於我們可以切片的東西,比如序列。如果 readings 是一個生成器,一個 zip 對象或其他任何類型的迭代器,那麼這段代碼就會失敗。

讓我們編寫一個輔助函數來修復代碼。

這是一個生成器函數,它為給定的迭代中的每個項目提供了當前項和下一項:

def with_next(iterable):

"""Yield (current, next_item) tuples for each item in iterable."""

iterator = iter(iterable)

current = next(iterator)

for next_item in iterator:

yield current, next_item

current = next_item

我們從可迭代對象中手動獲取一個迭代器,在它上面調用 next 來獲取第一項,然後循環遍歷迭代器獲取後續所有的項目,跟蹤後一個項目。這個函數不僅適用於序列,而且適用於任何類型迭代。

這段代碼和以前代碼是一樣的,但是我們使用的是輔助函數而不是手動跟蹤 next_item:

differences = []

for current, next_item in with_next(readings):

differences.append(next_item - current)

請注意,這段代碼不會掛在我們循環周圍的 next_item 上,with_next 生成器函數處理跟蹤 next_item 的工作。

還要注意,這段代碼已足夠緊湊,如果我們願意,我們甚至可以 將方法複製到列表推導中來 。

differences = [

(next_item - current)

for current, next_item in with_next(readings)

]

再次回顧循環問題

現在我們準備回到之前看到的那些奇怪的例子並試著找出到底發生了什麼。

問題 1:耗盡的迭代器

這裡我們有一個生成器對象 squares:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

如果我們把這個生成器傳遞給 tuple 構造函數,我們將會得到它的一個元組:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

>>> tuple(squares)

(1, 4, 9, 25, 49)

如果我們試著計算這個生成器中數字的和,使用 sum,我們就會得到 0:

>>> sum(squares)

0

這個生成器現在是空的:我們已經把它耗盡了。如果我們試著再次創建一個元組,我們會得到一個空元組:

>>> tuple(squares)

()

生成器是迭代器,迭代器是一次性的。它們就像 Hello Kitty Pez 分配器那樣不能重新加載。

問題 2:部分消耗一個迭代器

再次使用那個生成器對象 squares:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

如果我們詢問 9 是否在 squares 生成器中,我們會得到 True:

>>> 9 in squares

True

但是我們再次詢問相同的問題,我們會得到 False:

>>> 9 in squares

False

當我們詢問 9 是否在迭代器中時,Python 必須對這個生成器進行循環遍歷來找到 9。如果我們在檢查了 9 之後繼續循環遍歷,我們只會得到最後兩個數字,因為我們已經在找到 9 之前消耗了這些數字:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

>>> 9 in squares

True

>>> list(squares)

[25, 49]

詢問迭代器中是否包含某些東西將會部分地消耗迭代器。如果沒有循環遍歷迭代器,那麼是沒有辦法知道某個東西是否在迭代器中。

問題 3:拆包是迭代

當你在字典上循環時,你會得到鍵:

>>> counts = {'apples': 2, 'oranges': 1}

>>> for key in counts:

... print(key)

...

apples

oranges

當你對一個字典進行拆包時,你也會得到鍵:

>>> x, y = counts

>>> x, y

('apples', 'oranges')

循環依賴於迭代器協議,可迭代對象拆包也依賴於有迭代器協議。拆包一個字典與在字典上循環遍歷是一樣的,兩者都使用迭代器協議,所以在這兩種情況下都得到相同的結果。

回顧

序列是迭代器,但是不是所有的迭代器都是序列。當有人說“迭代器”這個詞時,你只能假設他們的意思是“你可以迭代的東西”。不要假設迭代器可以被循環遍歷兩次、詢問它們的長度或者索引。

迭代器是 Python 中最基本的可迭代形式。如果你想在代碼中做一個惰性迭代,請考慮迭代器,並考慮使用生成器函數或生成器表達式。

最後,請記住,Python 中的每一種迭代都依賴於迭代器協議,因此理解迭代器協議是理解 Python 中的循環的關鍵。

這裡有一些我推薦的相關文章和視頻:

  • Loop Like a Native , Ned Batchelder 在 PyCon 2013 的講演

  • Loop Better ,這篇文章是基於這個講演的

  • The Iterator Protocol: How For Loops Work ,我寫的關於迭代器協議的短文

  • Comprehensible Comprehensions ,關於推導和迭代器表達器的講演

  • Python: Range is Not an Iterator ,我關於範圍和迭代器的文章

  • Looping Like a Pro in Python ,DB 的 PyCon 2017 講演

本文是基於作者去年在 DjangoCon AU 、 PyGotham 和 North Bay Python 中發表的 Loop Better 演講。有關更多內容,請參加將於 2018 年 5 月 9 日至 17 日在 Columbus, Ohio 舉辦的 PYCON 。


via: https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python


分享到:


相關文章: