Python中迭代器與迭代器切片的詳細介紹

不言
發布: 2019-01-02 09:23:29
轉載
1738 人瀏覽過

這篇文章帶給大家的內容是關於Python中迭代器與迭代器切片的詳細介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

在前兩篇關於 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>

### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。</str_iterator>
登入後複製

這幾種方法中最值得一提的是iter() 方法,它是Python 的內建方法,其作用是將可迭代物件變成迭代器 。這句話可以解析出兩層意思:(1)可迭代物件跟迭代器是兩種東西;(2)可迭代物件能變成迭代器。

實際上,迭代器必然是可迭代對象,但可迭代對像不一定是迭代器。兩者有多大的差別呢?

Python中迭代器與迭代器切片的詳細介紹

如上圖藍圈所示,普通可迭代物件與迭代器的最關鍵區別可概括為:一同兩不同,所謂「一同”,即兩者都是可迭代的(__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.
登入後複製

以上是Python中迭代器與迭代器切片的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:segmentfault.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板