Python進階:迭代器與迭代器切片

来源:https://www.cnblogs.com/pythonista/archive/2018/12/30/10199346.html
-Advertisement-
Play Games

2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合併成一篇。合併後,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動。原系列的單篇就不刪除了,畢竟也是有單獨成篇的作用。特此聲明,請閱讀改進版—— Python進階:全面解讀高級特性之切片!https: ...


 

2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合併成一篇。合併後,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動。原系列的單篇就不刪除了,畢竟也是有單獨成篇的作用。特此聲明,請閱讀改進版—— Python進階:全面解讀高級特性之切片!https://mp.weixin.qq.com/s/IRAjR-KHZBPEEkdiofseGQ

 

 

在前兩篇關於 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") # <str_iterator at 0x1e2396d8f28>

### 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+本精選電子書。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、python assert的作用: 根據Python 官方文檔解釋(https://docs.python.org/3/reference/simple_stmts.html#assert), "Assert statements are a convenient way to insert d ...
  • Swagger2 方式,一定會讓你有不一樣的開發體驗:功能豐富 :支持多種註解,自動生成介面文檔界面,支持在界面測試API介面功能;及時更新 :開發過程中花一點寫註釋的時間,就可以及時的更新API文檔,省心省力;整合簡單 :通過添加pom依賴和簡單配置,內嵌於應用中就可同時發佈API介面文檔界面,不... ...
  • 2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合併成一篇。合併後,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動。原系列的單篇就不刪除了,畢竟也是有單獨成篇的作用。特此聲明,請閱讀改進版—— Python進階:全面解讀高級特性之切片!https: ...
  • 題意 "題目鏈接" Sol 裸的斜率優化,註意推導過程中的符號問題。 cpp include define Pair pair define MP(x, y) make_pair(x, y) define fi first define se second define int long long ...
  • 一個數組A中存有N(>0)個整數,在不允許使用另外數組的前提下,將每個整數迴圈向右移M(≥0)個位置,即將A中的數據由(A​0​​A​1​​⋯A​N−1​​)變換為(A​N−M​​⋯A​N−1​​A​0​​A​1​​⋯A​N−M−1​​)(最後M個數迴圈移至最前面的M個位置)。如果需要考慮程式移動數 ...
  • 讓我們用字母 B 來表示“百”、字母 S 表示“十”,用 12...n 來表示不為零的個位數字 n(<10),換個格式來輸出任一個不超過 3 位的正整數。例如 234 應該被輸出為 BBSSS1234,因為它有 2 個“百”、3 個“十”、以及個位的 4。 輸入格式: 每個測試輸入包含 1 個測試用 ...
  • 題意 "題目鏈接" Sol 重新看了一遍斜率優化,感覺又有了一些新的認識。 首先把土地按照$(w, h)$排序,用單調棧處理出每個位置第向左第一個比他大的位置,顯然這中間的元素是沒用的 設$f[i]$表示買了前$i$塊土地的最小花費 $f[i] = min_{j = 0}^{i 1}(f[j] + ...
  • 卡拉茲(Callatz)猜想已經在1001中給出了描述。在這個題目里,情況稍微有些複雜。 當我們驗證卡拉茲猜想的時候,為了避免重覆計算,可以記錄下遞推過程中遇到的每一個數。例如對 n=3 進行驗證的時候,我們需要計算 3、5、8、4、2、1,則當我們對 n=5、8、4、2 進行驗證的時候,就可以直接 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...