Python迭代和解析(2):迭代初探

  • 2020 年 1 月 23 日
  • 筆記

在Python中支持兩種循環格式:while和for。這兩種循環的類型不同:

  • while是通過條件判斷的真假來循環的
  • for是通過in的元素存在性測試來循環的

更通俗地說,while是普通的步進循環,for是迭代遍歷。

for的關鍵字在於"迭代"和"遍歷"。首先要有容器數據結構(如列表、字符串)存儲一些元素供迭代、遍歷,然後每次取下一個元素通過in來測試元素的存在性(從容器中取了元素為何還要測試?因為容器可能會在迭代過程中臨時發生改變),每次取一個,依次取下去,直到所有元素都被迭代完成,就完成了遍歷操作。

這種迭代模式是一種惰性的工作方式。當要掃描內存中放不下的大數據集時,需要找到一種惰性獲取數據項的方式,即按需一次獲取一個數據項,而不是一次性收集全部數據。從此可以看出這種迭代模式最顯著的優點是"內存佔用少",因為它從頭到尾迭代完所有數據的過程中都只需佔用一個元素的內存空間。

Python中的迭代和解析和for都息息相關,本文先初探迭代。

內置類型的迭代

for循環可以迭代列表、元組、字符串(str/bytes/bytearray)、集合、字典、文件等類型。

>>> for i in [1,2,3,4]: print(i * 2,end=" ")  ...  2 4 6 8    >>> for i in (1,2,3,4): print(i * 2,end=" ")  ...  2 4 6 8    >>> for i in "abcd": print(i * 2,end=" ")  ...  aa bb cc dd    >>> D=dict(a=1,b=2,c=3)  >>> for k in D:print("%s -> %s" % (k, D[k]))  ...  a -> 1  b -> 2  c -> 3

for循環其實比這更加通用。在Python中,只要是可迭代對象,或者更通俗地說是從左至右掃描對象的工具都可以進行這些迭代操作,這些工具有for、in成員測試、解析、map/zip等內置函數等。

關於什麼是可迭代對象,後文會詳細解釋。

文件迭代操作

要讀取一個文件有很多種方式:按位元組數讀取、按行讀取、按段落讀取、一次性全部讀取等等。如果不是深入的操作文件數據,按行讀、寫是最通用的方式。

以下是下面測試時使用的文件a.txt的內容:

first line  second line  third line

在Python中,readline()函數可以一次讀取一行,且每次都是前進式的讀取一行,讀到文件結尾的時候會返回空字符串。

>>> f = open('a.txt')  >>> f.readline()  'first linen'  >>> f.readline()  'second linen'  >>> f.readline()  'third linen'  >>> f.readline()  ''

readline()的操作就像是有一個指針,每次讀完一行就將指針指向那一行的後面做下標記,以便下次能從這裡開始繼續向後讀取一行。

除了readline(),open()打開的文件對象還有另一種方式__next__()可以一次向前讀取一行,只不過__next__()在讀取到文件結尾的時候不是返回空字符串,而是直接拋出迭代異常:

>>> f = open("a.txt")  >>> f.__next__()  'first linen'  >>> f.__next__()  'second linen'  >>> f.__next__()  'third linen'  >>> f.__next__()  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

內置函數next()會自動調用__next__(),也能進行迭代:

>>> f = open("a.txt")  >>> next(f)  'first linen'  >>> next(f)  'second linen'  >>> next(f)  'third linen'  >>> next(f)  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

要想再次讀取這個文件,只能先重置這個指針,比如重新打開這個文件可以重置指針。

open()打開的文件是一個可迭代對象,它有__next__(),它可以被for/in等迭代工具來操作,例如:

>>> 'first linen' in open('a.txt')  True

所以更好的按行讀取文件的方式是for line in open('file'),不用刻意使用readline()等函數去讀取。

>>> for line in open('a.txt'):  ...     print(line,end='')  ...  first line  second line  third line

上面的print()設置了end='',因為讀取每一行時會將換行符也讀入,而print默認是自帶換行符的,所以這裡要禁止print的終止符,否則每一行後將多一空行。

上面使用for line in open('a.txt')的方式是最好的,它每次只讀一行到內存,在需要讀下一行的時候再去文件中讀取,直到讀完整個文件也都只佔用了一行數據的內存空間。

也可以使用while去讀取文件,並:

>>> f=open('a.txt')  >>> while True:  ...     line = f.readline()  ...     if not line: break  ...     print(line,end='')  ...  first line  second line  third line

在Python中,使用for一般比while速度更快,它是C寫的,而while是Python虛擬機的解釋代碼。而且,for一般比while要更簡單,而往往Python中的簡單就意味着高效。

此外,還可以使用readlines()函數(和readline()不同,這是複數形式),它表示一次性讀取所有內容到一個列表中,每一行都是這個大列表的一個元素。

>>> lines = open('a.txt').readlines()  >>> lines  ['first linen', 'second linen', 'third linen']

因為存放到列表中了,所以也可以迭代readlines()讀取的內容:

>>> for line in open('a.txt').readlines():  ...     print(line,end='')  ...  first line  second line  third line

這種一次性全部讀取的方式在大多數情況下並非良方,如果是一個大文件,它會佔用大量內存,甚至可能會因為內存不足而讀取失敗。

但並非必須要選擇for line in open('a.txt')的方式,因為有些時候必須加載整個文件才能進行後續的操作,比如要排序文件,必須要擁有文件的所有數據才能進行排序。而且對於小文件來說,一次性讀取到一個列表中操作起來可能會更加方便,因為列表對象有很多好用的方法。所以,不能一概而論地選擇for line in open('a.txt')

手動迭代

Python 3.X提供了一個內置函數next(),它會自動調用對象的__next__(),所以藉助它可以進行手動迭代。

>>> f=open('a.txt')  >>> next(f)  'first linen'  >>> next(f)  'second linen'  >>> next(f)  'third linen'  >>> next(f)  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

可迭代對象、迭代協議和迭代工具的工作流程

這裡只是解釋這幾個概念和__iter__()__next__(),在後面會手動編寫這兩個方法來自定義迭代對象。

什麼是迭代協議

參考手冊:https://docs.python.org/3.7/library/stdtypes.html#iterator-types

只要某個類型(類)定義了__iter__()__next__()方法就表示支持迭代協議。

__iter__()需要返回一個可迭代對象。只要定義了__iter__()就表示能夠通過for/in/map/zip等迭代工具進行對應的迭代,也可以手動去執行迭代操作

for x in Iterator  X in Iterator

同時,可迭代對象還可以作為某些函數參數,例如將可迭代對象構建成一個列表list(Iterator)來查看這個可迭代對象會返回哪些數據:

L = list(Iterator)

需要注意的是,for/in/map/zip等迭代工具要操作的對象並不一定要實現__iter__(),實現了__getitem__()也可以。__getitem__()是數值索引迭代的方式,它的優先級低於__iter__()

__next__()方法用於向前一次返回一個結果,並且在前進到結尾的地方觸發StopIteration異常。

再次說明,只要實現了這兩個方法的類型,就表示支持迭代協議,可以被迭代。

例如open()的文件類型:

>>> f=open('a.txt')  >>> dir(f)  [... '__iter__', ... '__next__', ...]

但如果看下列表類型、元組、字符串等容器類型的屬性列表,會發現沒有它們只有__iter__(),並沒有__next__()

>>> dir(list)  [... '__iter__', ...]    >>> dir(tuple)  [... '__iter__', ...]    >>> dir(str)  [... '__iter__', ...']    >>> dir(set)  [... '__iter__', ...]    >>> dir(dict)  [... '__iter__', ...]

但為什麼它們能進行迭代呢?繼續看下文"可迭代對象"的解釋。

什麼是迭代對象和迭代器

對於前面的容器類型(list/set/str/tuple/dict)只有__iter__()而沒有__next__(),但卻可以進行迭代操作的原因,是這些容器類型的__iter__()返回了一個可迭代對象,而這些可迭代對象才是真的支持迭代協議、可進行迭代的對象。

>>> L=[1,2,3,4]  >>> L_iter = L.__iter__()    >>> L_iter  <list_iterator object at 0x000001E53A105400>    >>> dir(L_iter)  [... '__iter__', ... '__next__', ...]    >>> L.__next__()  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  AttributeError: 'list' object has no attribute '__next__'    >>> L_iter.__next__()  1  >>> L_iter.__next__()  2  >>> L_iter.__next__()  3  >>> L_iter.__next__()  4

所以,對於容器類型,它們是通過__iter__()來返回一個迭代對象,然後這個可迭代對象需要支持迭代協議(有__iter__()__next__()方法)。

也就是說,所謂的迭代對象是通過__iter__()來返回的。迭代對象不一定可迭代,只有支持迭代協議的迭代對象才能稱為可迭代對象

迭代器則是迭代對象的一種類型統稱,只要是可迭代對象,都可以稱為迭代器。所以,一般來說,迭代器和可迭代對象是可以混用的概念。但嚴格點定義,迭代對象是iter()返回的,迭代器是__iter__()返回的,所以它們的關係是:從迭代對象中獲取迭代器(可迭代對象)。

如果要自己定義迭代對象類型,不僅需要返回可迭代對象,還需要這個可迭代對象同時實現了__iter__()__next__()

正如open()返回的類型,它有__iter__()和__next__(),所以它支持迭代協議,可以被迭代。再者,它的__iter__()返回的是自身,而自身又實現了這兩個方法,所以它是可迭代對象:

>>> f = open('a.txt')  >>> f.__iter__() is f  True

所以,如果想要知道某個對象是否可迭代,可以直接調用iter()來測試,如果它不拋出異常,則說明可迭代(儘管還要求實現__next__())。

迭代工具的工作流程

像for/in/map/zip等迭代工具,它們的工作流程大致遵循這些過程(並非一定如此):

  1. 在真正開始迭代之前,首先會通過iter(X)內置函數獲取到要操作的迭代對象Y
    • 例如it = iter([1,2,3,4])
    • iter(X)會調用X的__iter__(),前面說過這個方法要求返回迭代對象
    • 如果沒有__iter__(),則iter()轉而調用__getitem__()來進行索引迭代
  2. 獲取到迭代對象後,開始進入迭代過程。在迭代過程中,每次都調用next(Y)內置函數來生成一個結果,而next()會自動調用Y的__next__()

如果類型對象自身就實現了__iter__()__next__(),則這個類型的可迭代對象就是自身。就像open()返回的文件類型一樣。

如果自身只是實現了__iter__()而沒有__next__(),那麼它的__iter__()就需要返回實現了__iter__()__next__()的類型對象。這種類型的對象自身不是迭代器,就像內置的各種可迭代容器類型一樣。

關於iter(), __iter__(), next(), __next__(),它們兩兩的作用是一致的,只不過基於類設計的考慮,將__iter__()__next__()作為了通用的類型對象屬性,而額外添加了iter()和next()來調用它們。

for/map/in/zip等迭代工具是自動進行迭代的,但既然理解了可迭代對象,我們也可以手動去循環迭代:

>>> L=[1,2,3,4]  >>> for i in L:print(i,end=" ")  ...  1 2 3 4    L = [1,2,3,4]  I = iter(L)  while True:      try:          x = next(I)      except StopIteration:          break      print(x,end=" ")

注意:

  1. 每一個迭代對象都是一次性資源,迭代完後就不能再次從頭開始迭代,如果想要再次迭代,必須使用iter()重新獲取迭代對象
  2. 每次迭代時,都會標記下當前所迭代的位置,以便下次從下一個指針位置處繼續迭代

可迭代對象示例:range和enumerate

range()返回的內容是一個可迭代對象,作為可迭代對象,可以進行上面所描述的一些操作。

>>> 3 in range(5)  True    >>> for i in range(5):print(i,end=" ")  ...  0 1 2 3 4    >>> list(range(5))  [0, 1, 2, 3, 4]    >>> R = range(5)  >>> I = iter(R)  >>> next(I)  0  >>> next(I)  1  >>> next(I)  2  >>> next(I)  3  >>> next(I)  4  >>> next(I)  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

enumerate()返回的也是可迭代對象:

>>> E = enumerate('hello')  >>> E  <enumerate object at 0x000001EF6BFD1F78>  >>> I = iter(E)  >>> next(I)  (0, 'h')  >>> next(I)  (1, 'e')  >>> next(I)  (2, 'l')  >>> next(I)  (3, 'l')  >>> next(I)  (4, 'o')  >>> next(I)  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

可迭代對象實例:字典的可迭代視圖

字典自身有__iter__(),所以dict也是可迭代的對象,只不過它所返回的可迭代對象是dict的key。

>>> D = dict(one=1,two=2,three=3,four=4)  >>> I = iter(D)  >>> next(I)  'one'  >>> next(I)  'two'  >>> next(I)  'three'  >>> next(I)  'four'  >>> next(I)  Traceback (most recent call last):    File "<stdin>", line 1, in <module>  StopIteration

除此之外,dict還支持其它可迭代的字典視圖keys()、values()、items()。

>>> hasattr(D.keys(),"__iter__")  True  >>> hasattr(D.values(),"__iter__")  True  >>> hasattr(D.items(),"__iter__")  True