Heim  >  Artikel  >  Backend-Entwicklung  >  Detaillierte Einführung in Iteratoren und Iterator-Slicing in Python

Detaillierte Einführung in Iteratoren und Iterator-Slicing in Python

不言
不言nach vorne
2019-01-02 09:23:291708Durchsuche

Dieser Artikel bietet Ihnen eine detaillierte Einführung in Iteratoren und Iterator-Slices. Ich hoffe, dass er für Freunde hilfreich ist.

In den ersten beiden Artikeln über Python-Slicing haben wir die grundlegende Verwendung, die erweiterte Verwendung, Missverständnisse des Slicings und die Art und Weise kennengelernt, wie benutzerdefinierte Objekte die Slicing-Nutzung implementieren (siehe am Ende des Artikels für verwandte Links). Dieser Artikel ist der dritte in der Slicing-Reihe und sein Hauptinhalt ist das Iterator-Slicing.

Iterator ist eine einzigartige erweiterte Funktion in Python, und Slicing ist ebenfalls eine erweiterte Funktion. Was wird das Ergebnis der Kombination der beiden sein?

1. Iteration und Iteratoren

Zunächst müssen einige Grundkonzepte geklärt werden: Iteration, iterierbare Objekte und Iteratoren.

迭代 ist eine Möglichkeit, Containertypobjekte (wie Zeichenfolgen, Listen, Wörterbücher usw.) zu durchlaufen. Wenn wir beispielsweise sagen, dass eine Zeichenfolge „abc“ iteriert, meinen wir die Iteration von links nach rechts . Der Prozess, bei dem alle Zeichen einzeln extrahiert werden. (PS: Das Wort „Iteration“ bedeutet auf Chinesisch wiederholte Zyklen und schichtweises Fortschreiten, aber in Python sollte dieses Wort als unidirektionale horizontale Linearität verstanden werden. Wenn Sie damit nicht vertraut sind, empfehle ich es Sie verwenden es direkt. (Verstanden als Durchquerung)

Wie schreibt man also Anweisungen für iterative Operationen? Die gebräuchlichste Schreibsyntax ist die for-Schleife.

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

Die for-Schleife kann den Iterationsprozess implementieren, aber nicht alle Objekte können in der for-Schleife verwendet werden. Wenn beispielsweise die Zeichenfolge „abc“ im obigen Beispiel durch eine beliebige Ganzzahl ersetzt wird, wird ein Fehler angezeigt gemeldet werden: „int“-Objekt ist nicht iterierbar.

Das Wort „iterable“ in dieser Fehlermeldung bezieht sich auf „iterable“, d. h. der int-Typ ist nicht iterierbar. Der String-Typ ist iterierbar, und ebenso sind Typen wie Listen, Tupel und Wörterbücher alle iterierbar.

Wie kann man also feststellen, ob ein Objekt iterierbar ist? Warum sind sie iterierbar? Wie mache ich ein Objekt iterierbar?

Um ein Objekt iterierbar zu machen, müssen Sie das iterierbare Protokoll implementieren, das heißt, Sie müssen die __iter__() magische Methode implementieren. Mit anderen Worten, solange das Objekt, das diese magische Methode implementiert, ein ist iterierbares Objekt.

Wie kann man dann feststellen, ob ein Objekt diese Methode implementiert? Ich weiß, dass es neben der oben genannten for-Schleife vier weitere Methoden gibt:

# 方法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>

Die bemerkenswerteste unter diesen Methoden ist die iter()-Methode, eine integrierte Methode von Python, deren Funktion Iterierbare Objekte in Iteratoren umwandeln. Dieser Satz kann in zwei Bedeutungen zerlegt werden: (1) iterierbare Objekte und Iteratoren sind zwei verschiedene Dinge; (2) iterierbare Objekte können zu Iteratoren werden.

Tatsächlich müssen Iteratoren iterierbare Objekte sein, aber iterierbare Objekte sind nicht unbedingt Iteratoren. Wie groß ist der Unterschied zwischen den beiden?

Detaillierte Einführung in Iteratoren und Iterator-Slicing in Python

Wie im blauen Kreis im Bild oben gezeigt, kann der kritischste Unterschied zwischen gewöhnlichen iterierbaren Objekten und Iteratoren wie folgt zusammengefasst werden: die gleichen zwei sind unterschiedlich, das sogenannte „das Gleiche“, das heißt, beide sind iterierbar (__iter__). Die sogenannten „zwei Unterschiede“ bedeuten, dass das iterierbare Objekt nach der Konvertierung in einen Iterator einige Attribute verliert (__getitem__). und fügen Sie auch einige Attribute hinzu (__next__).

Werfen Sie zunächst einen Blick auf das hinzugefügte Attribut __next__. Es ist der Schlüssel dafür, warum ein Iterator ein Iterator ist. Tatsächlich definieren wir ein Objekt, das sowohl die __iter__-Methode als auch die __next__-Methode implementiert .

Mit diesem zusätzlichen Attribut können iterierbare Objekte ihren eigenen Iterations-/Durchlaufprozess implementieren, ohne auf eine externe for-Schleifensyntax zurückgreifen zu müssen. Ich habe zwei Konzepte erfunden, um diese beiden Durchquerungsprozesse zu beschreiben (PS: Zum besseren Verständnis wird es hier als Durchquerung bezeichnet, kann aber tatsächlich als Iteration bezeichnet werden): Durchquerung bezieht sich auf die Durchquerung, die durch externe Grammatik implementiert wird, und Selbstdurchquerung bezieht sich auf die Durchquerung, die durch externe Grammatik implementiert wird durch eigene Methoden.

Mit diesen beiden Konzepten sagen wir, dass ein iterierbares Objekt ein Objekt ist, das „von ihm durchlaufen“ werden kann, und ein Iterator ein Objekt ist, das auf dieser Basis auch „selbst durchlaufen“ werden kann.

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
Wie aus dem obigen Beispiel ersichtlich ist, besteht der Vorteil des Iterators darin, dass er die Selbstdurchquerung unterstützt. Gleichzeitig zeichnet er sich durch eine Einweg-Nichtschleife aus. Bei erneutem Aufruf wird ein Fehler gemeldet.

In diesem Zusammenhang habe ich mir eine Analogie ausgedacht: Ein gewöhnliches iterierbares Objekt ist wie ein Geschossmagazin. Es bewegt sich, indem es das Geschoss herausnimmt und nach Abschluss des Vorgangs wieder einsetzt, sodass es wiederholt durchquert werden kann (das). Wenn die for-Schleife mehrmals aufgerufen wird, wird das gleiche Ergebnis zurückgegeben. Der Iterator ist wie eine Waffe, die mit einem Magazin geladen ist und nicht abgenommen werden kann. Dies ist eine verbrauchbare Durchquerung und kann nicht wiederverwendet werden (d. h. der Durchlauf hat ein Ende. ).

Ich habe so viel geschrieben, lassen Sie es mich ein wenig zusammenfassen:

Iteration ist eine Methode zum Durchlaufen von Elementen. Es gibt zwei Arten der externen Iteration und der internen Iteration. Objekte, die externe Iteration unterstützen (durchlaufen) Es handelt sich um ein iterierbares Objekt, und ein Objekt, das auch interne Iteration (Selbstdurchquerung) unterstützt, ist ein Iterator. Gemäß der Verbrauchsmethode kann es in wiederverwendbare Iteration und einmalige Iteration unterteilt werden . Gewöhnliche iterierbare Objekte sind wiederverwendbar und Iteratoren Das Gerät ist wegwerfbar.

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.

Das obige ist der detaillierte Inhalt vonDetaillierte Einführung in Iteratoren und Iterator-Slicing in Python. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen