摘要:本文是切片系列的第三篇,主要內容是迭代器切片。實際上,迭代器必然是可迭代對象,但可迭代對象不一定是迭代器。這是迭代器切片最具想象力的用途場景。考慮到文件對象天然就是迭代器,我們可以使用迭代器切片先行截取,然后再處理,如此效率將大大地提升。
2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合并成一篇。合并后,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動。原系列的單篇就不刪除了,畢竟也是有多帶帶成篇的作用。特此聲明,請閱讀改進版—— Python進階:全面解讀高級特性之切片!https://mp.weixin.qq.com/s/IR...
在前兩篇關于 Python 切片的文章中,我們學習了切片的基礎用法、高級用法、使用誤區,以及自定義對象如何實現切片用法(相關鏈接見文末)。本文是切片系列的第三篇,主要內容是迭代器切片。
迭代器是 Python 中獨特的一種高級特性,而切片也是一種高級特性,兩者相結合,會產生什么樣的結果呢?
1、迭代與迭代器首先,有幾個基本概念要澄清:迭代、可迭代對象、迭代器。
迭代 是一種遍歷容器類型對象(例如字符串、列表、字典等等)的方式,例如,我們說迭代一個字符串“abc”,指的就是從左往右依次地、逐個地取出它的全部字符的過程。(PS:漢語中迭代一詞有循環反復、層層遞進的意思,但 Python 中此詞要理解成單向水平線性 的,如果你不熟悉它,我建議直接將其理解為遍歷。)
那么,怎么寫出迭代操作的指令呢?最通用的書寫語法就是 for 循環。
# for循環實現迭代過程 for char in "abc": print(char, end=" ") # 輸出結果:a b c
for 循環可以實現迭代的過程,但是,并非所有對象都可以用于 for 循環,例如,上例中若將字符串“abc”換成任意整型數字,則會報錯: "int" object is not iterable .
這句報錯中的單詞“iterable”指的是“可迭代的”,即 int 類型不是可迭代的。而字符串(string)類型是可迭代的,同樣地,列表、元組、字典等類型,都是可迭代的。
那怎么判斷一個對象是否可迭代呢?為什么它們是可迭代的呢?怎么讓一個對象可迭代呢?
要使一個對象可迭代,就要實現可迭代協議,即需要實現__iter__() 魔術方法,換言之,只要實現了這個魔術方法的對象都是可迭代對象。
那怎么判斷一個對象是否實現了這個方法呢?除了上述的 for 循環外,我知道還有四種方法:
# 方法1:dir()查看__iter__ dir(2) # 沒有,略 dir("abc") # 有,略 # 方法2:isinstance()判斷 import collections isinstance(2, collections.Iterable) # False isinstance("abc", collections.Iterable) # True # 方法3:hasattr()判斷 hasattr(2,"__iter__") # False hasattr("abc","__iter__") # True # 方法4:用iter()查看是否報錯 iter(2) # 報錯:"int" object is not iterable iter("abc") #### PS:判斷是否可迭代,還可以查看是否實現__getitem__,為方便描述,本文從略。
這幾種方法中最值得一提的是 iter() 方法,它是 Python 的內置方法,其作用是將可迭代對象變成迭代器 。這句話可以解析出兩層意思:(1)可迭代對象跟迭代器是兩種東西;(2)可迭代對象能變成迭代器。
實際上,迭代器必然是可迭代對象,但可迭代對象不一定是迭代器。兩者有多大的區別呢?
如上圖藍圈所示,普通可迭代對象與迭代器的最關鍵區別可概括為:一同兩不同 ,所謂“一同”,即兩者都是可迭代的(__iter__),所謂“兩不同”,即可迭代對象在轉化為迭代器后,它會丟失一些屬性(__getitem__),同時也增加一些屬性(__next__)。
首先看看增加的屬性 __next__ , 它是迭代器之所以是迭代器的關鍵,事實上,我們正是把同時實現了 __iter__ 方法 和 __next__ 方法的對象定義為迭代器的。
有了多出來的這個屬性,可迭代對象不需要借助外部的 for 循環語法,就能實現自我的迭代/遍歷過程。我發明了兩個概念來描述這兩種遍歷過程(PS:為了易理解,這里稱遍歷,實際也可稱為迭代):它遍歷 指的是通過外部語法而實現的遍歷,自遍歷 指的是通過自身方法實現的遍歷。
借助這兩個概念,我們說,可迭代對象就是能被“它遍歷”的對象,而迭代器是在此基礎上,還能做到“自遍歷”的對象。
ob1 = "abc" ob2 = iter("abc") ob3 = iter("abc") # ob1它遍歷 for i in ob1: print(i, end = " ") # a b c for i in ob1: print(i, end = " ") # a b c # ob1自遍歷 ob1.__next__() # 報錯: "str" object has no attribute "__next__" # ob2它遍歷 for i in ob2: print(i, end = " ") # a b c for i in ob2: print(i, end = " ") # 無輸出 # ob2自遍歷 ob2.__next__() # 報錯:StopIteration # ob3自遍歷 ob3.__next__() # a ob3.__next__() # b ob3.__next__() # c ob3.__next__() # 報錯:StopIteration
通過上述例子可看出,迭代器的優勢在于支持自遍歷,同時,它的特點是單向非循環的,一旦完成遍歷,再次調用就會報錯。
對此,我想到一個比方:普通可迭代對象就像是子彈匣,它遍歷就是取出子彈,在完成操作后又裝回去,所以可以反復遍歷(即多次調用for循環,返回相同結果);而迭代器就像是裝載了子彈匣且不可拆卸的槍,進行它遍歷或者自遍歷都是發射子彈,這是消耗性的遍歷,是無法復用的(即遍歷會有盡頭)。
寫了這么多,稍微小結一下:迭代是一種遍歷元素的方式,按照實現方式劃分,有外部迭代與內部迭代兩種,支持外部迭代(它遍歷)的對象就是可迭代對象,而同時還支持內部迭代(自遍歷)的對象就是迭代器;按照消費方式劃分,可分為復用型迭代與一次性迭代,普通可迭代對象是復用型的,而迭代器是一次性的。
2、迭代器切片前面提到了“一同兩不同”,最后的不同是,普通可迭代對象在轉化成迭代器的過程中會丟失一些屬性,其中關鍵的屬性是 __getitem__ 。在《Python進階:自定義對象實現切片功能》中,我曾介紹了這個魔術方法,并用它實現了自定義對象的切片特性。
那么問題來了:為什么迭代器不繼承這個屬性呢?
首先,迭代器使用的是消耗型的遍歷,這意味著它充滿不確定性,即其長度與索引鍵值對是動態衰減的,所以很難 get 到它的 item ,也就不再需要 __getitem__ 屬性了。其次,若強行給迭代器加上這個屬性,這并不合理,正所謂強扭的瓜不甜......
由此,新的問題來了:既然會丟失這么重要的屬性(還包括其它未標識的屬性),為什么還要使用迭代器呢?
這個問題的答案在于,迭代器擁有不可替代的強大的有用的功能,使得 Python 要如此設計它。限于篇幅,此處不再展開,后續我會專門填坑此話題。
還沒完,死纏爛打的問題來了:能否令迭代器擁有這個屬性呢,即令迭代器繼續支持切片呢?
hi = "歡迎關注公眾號:Python貓" it = iter(hi) # 普通切片 hi[-7:] # Python貓 # 反例:迭代器切片 it[-7:] # 報錯:"str_iterator" object is not subscriptable
迭代器因為缺少__getitem__ ,因此不能使用普通的切片語法。想要實現切片,無非兩種思路:一是自己造輪子,寫實現的邏輯;二是找到封裝好的輪子。
Python 的 itertools 模塊就是我們要找的輪子,用它提供的方法可輕松實現迭代器切片。
import itertools # 例1:簡易迭代器 s = iter("123456789") for x in itertools.islice(s, 2, 6): print(x, end = " ") # 輸出:3 4 5 6 for x in itertools.islice(s, 2, 6): print(x, end = " ") # 輸出:9 # 例2:斐波那契數列迭代器 class Fib(): def __init__(self): self.a, self.b = 1, 1 def __iter__(self): while True: yield self.a self.a, self.b = self.b, self.a + self.b f = iter(Fib()) for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:2 3 5 8 for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:34 55 89 144
itertools 模塊的 islice() 方法將迭代器與切片完美結合,終于回答了前面的問題。然而,迭代器切片跟普通切片相比,前者有很多局限性。首先,這個方法不是“純函數”(純函數需遵守“相同輸入得到相同輸出”的原則,之前在《來自Kenneth Reitz大神的建議:避免不必要的面向對象編程》提到過);其次,它只支持正向切片,且不支持負數索引,這都是由迭代器的損耗性所決定的。
那么,我不禁要問:itertools 模塊的切片方法用了什么實現邏輯呢?下方是官網提供的源碼:
def islice(iterable, *args): # islice("ABCDEFG", 2) --> A B # islice("ABCDEFG", 2, 4) --> C D # islice("ABCDEFG", 2, None) --> C D E F G # islice("ABCDEFG", 0, None, 2) --> A C E G s = slice(*args) # 索引區間是[0,sys.maxsize],默認步長是1 start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1 it = iter(range(start, stop, step)) try: nexti = next(it) except StopIteration: # Consume *iterable* up to the *start* position. for i, element in zip(range(start), iterable): pass return try: for i, element in enumerate(iterable): if i == nexti: yield element nexti = next(it) except StopIteration: # Consume to *stop*. for i, element in zip(range(i + 1, stop), iterable): pass
islice() 方法的索引方向是受限的,但它也提供了一種可能性:即允許你對一個無窮的(在系統支持范圍內)迭代器進行切片的能力。這是迭代器切片最具想象力的用途場景。
除此之外,迭代器切片還有一個很實在的應用場景:讀取文件對象中給定行數范圍的數據。
在《給Python學習者的文件讀寫指南(含基礎與進階,建議收藏)》里,我介紹了從文件中讀取內容的幾種方法:readline() 比較雞肋,不咋用;read() 適合讀取內容較少的情況,或者是需要一次性處理全部內容的情況;而 readlines() 用的較多,比較靈活,每次迭代讀取內容,既減少內存壓力,又方便逐行對數據處理。
雖然 readlines() 有迭代讀取的優勢,但它是從頭到尾逐行讀取,若文件有幾千行,而我們只想要讀取少數特定行(例如第1000-1009行),那它還是效率太低了。考慮到文件對象天然就是迭代器 ,我們可以使用迭代器切片先行截取,然后再處理,如此效率將大大地提升。
# test.txt 文件內容 """ 貓 Python貓 python is a cat. this is the end. """ from itertools import islice with open("test.txt","r",encoding="utf-8") as f: print(hasattr(f, "__next__")) # 判斷是否迭代器 content = islice(f, 2, 4) for line in content: print(line.strip()) ### 輸出結果: True python is a cat. this is the end.3、小結
好啦,今天的學習就到這,小結一下:迭代器是一種特殊的可迭代對象,可用于它遍歷與自遍歷,但遍歷過程是損耗型的,不具備循環復用性,因此,迭代器本身不支持切片操作;通過借助 itertools 模塊,我們能實現迭代器切片,將兩者的優勢相結合,其主要用途在于截取大型迭代器(如無限數列、超大文件等等)的片段,實現精準的處理,從而大大地提升性能與效率。
切片系列:
《Python進階:切片的誤區與高級用法》
《Python進階:自定義對象實現切片功能》
相關鏈接:
《官網的itertools模塊介紹》
《來自Kenneth Reitz大神的建議:避免不必要的面向對象編程》
《給Python學習者的文件讀寫指南(含基礎與進階,建議收藏)》
-----------------
本文原創并首發于微信公眾號【Python貓】,后臺回復“愛學習”,免費獲得20+本精選電子書。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/42890.html
摘要:本文是切片系列的第三篇,主要內容是迭代器切片。實際上,迭代器必然是可迭代對象,但可迭代對象不一定是迭代器。這是迭代器切片最具想象力的用途場景。考慮到文件對象天然就是迭代器,我們可以使用迭代器切片先行截取,然后再處理,如此效率將大大地提升。 2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合并成一篇。合并后,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章...
摘要:抓住了迭代器模式的本質,即是迭代,賦予了它極高的地位。輸出結果輸出結果小結迭代器模式幾乎是種設計模式中最常用的設計模式,本文主要介紹了是如何運用迭代器模式,并介紹了模塊生成迭代器的種方法,以及種生成迭代器的內置方法。 showImg(https://segmentfault.com/img/bVbmv7W?w=4272&h=2848); 在軟件開發領域中,人們經常會用到這一個概念——設...
閱讀 3028·2021-09-08 10:43
閱讀 1030·2019-08-30 15:53
閱讀 964·2019-08-30 13:51
閱讀 836·2019-08-29 14:03
閱讀 796·2019-08-26 18:35
閱讀 1228·2019-08-26 13:38
閱讀 1580·2019-08-26 10:34
閱讀 3497·2019-08-26 10:21