ホームページ  >  記事  >  バックエンド開発  >  Python のイテレータとイテレータ スライスの詳細な紹介

Python のイテレータとイテレータ スライスの詳細な紹介

不言
不言転載
2019-01-02 09:23:291718ブラウズ

この記事では、Python のイテレータとイテレータ スライスについて詳しく説明します。一定の参考値があります。必要な友人は参照できます。お役に立てば幸いです。

Python スライスに関する最初の 2 つの記事では、基本的な使用法、高度な使用法、スライスの誤解、およびカスタム オブジェクトがスライスの使用法を実装する方法について学びました (関連リンクについては記事の最後を参照してください)。この記事はスライシング シリーズの 3 番目であり、主な内容はイテレータのスライシングです。

イテレータは Python 独自の高度な機能であり、スライスも高度な機能です。この 2 つを組み合わせた結果はどうなるでしょうか?

1. 反復と反復子

まず、反復、反復可能オブジェクト、反復子という基本的な概念を明確にする必要があります。

Iteration は、コンテナ タイプのオブジェクト (文字列、リスト、辞書など) を走査する方法です。たとえば、文字列 "abc" を反復すると言う場合、それは次のことを指します。から開始 すべての文字を左から右に 1 つずつ抽出するプロセス。 (追記: 中国語で「反復」という言葉は、繰り返しのサイクルと層ごとの進行を意味しますが、Python ではこの言葉は 一方通行の水平線形 として理解する必要があります。よく知らない場合は、直接使用することをお勧めします。トラバーサルとして理解します。)

それでは、反復操作の命令をどのように書くのでしょうか?最も一般的な記述構文は for ループです。

# for循环实现迭代过程
for char in "abc":
    print(char, end=" ")
# 输出结果:a b c

for ループは反復プロセスを実装できますが、すべてのオブジェクトを for ループで使用できるわけではありません。たとえば、上記の例で文字列「abc」が任意の整数に置き換えられると、エラーが発生します。報告される: 'int' オブジェクトは反復可能ではありません。

このエラー メッセージの「反復可能」という単語は「反復可能」、つまり int 型は反復可能ではないことを指します。文字列型は反復可能であり、同様に、リスト、タプル、辞書などの型はすべて反復可能です。

オブジェクトが反復可能かどうかを判断するにはどうすればよいですか?なぜ反復可能なのでしょうか?オブジェクトを反復可能にする方法は?

オブジェクトを反復可能にするには、反復可能プロトコルを実装する必要があります、つまり、__iter__() マジック メソッドを実装する必要があります。このマジック メソッドを実装するのは反復可能なオブジェクトです。

オブジェクトがこのメソッドを実装しているかどうかを判断するにはどうすればよいですか?上記の for ループに加えて、私は他に 4 つのメソッドを知っています:

# 方法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 の組み込みメソッドであり、その関数は次のとおりです反復可能なオブジェクトをイテレーターに変換します。この文は 2 つの意味に解析できます: (1) 反復可能オブジェクトと反復子は 2 つの異なるものです; (2) 反復可能オブジェクトは反復子になることができます。

実際、反復子は反復可能オブジェクトである必要がありますが、反復可能オブジェクトは必ずしも反復子である必要はありません。両者の違いはどれくらいですか?

Python のイテレータとイテレータ スライスの詳細な紹介

#上の図の青い円で示されているように、通常の反復可能オブジェクトとイテレーターの最も重要な違いは次のように要約できます。同じで、もう一方は異なります。いわゆる「一方は同じ」、つまり、両方とも反復可能 (__iter__) です。いわゆる「2 つの違い」とは、反復可能なオブジェクトが反復子に変換された後、それが反復可能であることを意味します。一部の属性が失われ (__getitem__)、また一部の属性が追加されます (__next__)。

まず、追加された属性 __next__ を見てください。これがイテレータがイテレータである理由の鍵です。実際、__iter__ メソッドと __next__ メソッドの両方をイテレータとして実装するオブジェクトを定義します。 。

この追加属性を使用すると、反復可能オブジェクトは、外部の for ループ構文に頼ることなく、独自の反復/走査プロセスを実装できます。これら 2 つのトラバーサル プロセスを説明するために、私は 2 つの概念を発明しました (追記: 理解を容易にするために、ここではトラバーサルと呼んでいますが、実際には反復と呼ぶこともできます)。トラバーサルは外部文法によって実装されたトラバーサルを指し、自己トラバーサルは実装されたトラバーサルを指します。独自のメソッドを通じて。

これら 2 つの概念の助けを借りて、反復可能オブジェクトは「それによってトラバース」できるオブジェクトであり、イテレータはこれに基づいて「自己トラバース」もできるオブジェクトであると言います。 。

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種類があります。外部反復をサポートするオブジェクト (トラバース) 反復可能なオブジェクトであり、内部反復 (自己トラバース) もサポートするオブジェクトはイテレーターであり、消費方法に応じて、再利用可能な反復と 1 回限りの反復に分けることができます。通常の反復可能なオブジェクトは再利用可能であり、反復子は使い捨てです。

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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。