Maison  >  Article  >  développement back-end  >  Introduction détaillée aux itérateurs et au découpage d'itérateurs en Python

Introduction détaillée aux itérateurs et au découpage d'itérateurs en Python

不言
不言avant
2019-01-02 09:23:291671parcourir

Cet article vous apporte une introduction détaillée aux itérateurs et aux tranches d'itérateur en Python. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Dans les deux premiers articles sur le découpage Python, nous avons appris l'utilisation de base, l'utilisation avancée, les malentendus sur le découpage et la façon dont les objets personnalisés implémentent l'utilisation du découpage (voir la fin de l'article pour les liens connexes). Cet article est le troisième de la série sur le découpage, et son contenu principal est le découpage par itérateur.

Iterator est une fonctionnalité avancée unique en Python, et le découpage est également une fonctionnalité avancée. Quel sera le résultat de la combinaison des deux ?

1. Itération et itérateurs

Tout d'abord, il y a plusieurs concepts de base à clarifier : l'itération, les objets itérables et les itérateurs.

迭代 est un moyen de parcourir des objets de type conteneur (tels que des chaînes, des listes, des dictionnaires, etc. Par exemple, lorsque nous disons itérer une chaîne "abc", nous entendons itérer de gauche à droite). . Le processus de suppression de tous ses caractères un par un. (PS : Le mot « itération » en chinois signifie cycles répétés et progression couche par couche, mais en Python, ce mot doit être compris comme linéaire horizontal à sens unique. Si vous ne le connaissez pas, Je vous suggère de l'utiliser directement. Compris comme traversée)

Alors, comment écrire des instructions pour les opérations itératives ? La syntaxe d'écriture la plus courante est la boucle for.

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

La boucle for peut implémenter le processus itératif, mais tous les objets ne peuvent pas être utilisés dans la boucle for. Par exemple, dans l'exemple ci-dessus, si la chaîne "abc" est remplacée par un entier, ce sera Erreur : l'objet 'int' n'est pas itérable.

Le mot « itérable » dans cette erreur fait référence à « itérable », c'est-à-dire que le type int n'est pas itérable. Le type chaîne est itérable, et de même, les types tels que les listes, les tuples et les dictionnaires sont tous itérables.

Alors comment déterminer si un objet est itérable ? Pourquoi sont-ils itérables ? Comment rendre un objet itérable ?

Pour rendre un objet itérable, vous devez implémenter le protocole itérable, c'est-à-dire que vous devez implémenter la __iter__() méthode magique En d'autres termes, tant que l'objet qui implémente cette méthode magique est un. objet itérable.

Alors comment déterminer si un objet implémente cette méthode ? En plus de la boucle for mentionnée ci-dessus, je connais quatre autres méthodes :

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

La plus remarquable de ces méthodes est la méthode iter(), qui est une méthode intégrée de Python et sa fonction est Convertir un objet itérable en itérateur. Cette phrase peut être analysée en deux sens : (1) les objets itérables et les itérateurs sont deux choses différentes (2) les objets itérables peuvent devenir des itérateurs ;

En fait, les itérateurs doivent être des objets itérables, mais les objets itérables ne sont pas nécessairement des itérateurs. Quelle est la différence entre les deux ?

Introduction détaillée aux itérateurs et au découpage ditérateurs en Python

Comme le montre le cercle bleu dans l'image ci-dessus, la différence la plus critique entre les objets itérables ordinaires et les itérateurs peut être résumée comme suit : les deux mêmes sont différents, ce qu'on appelle "le même", c'est-à-dire que les deux sont itérables (__iter__). Les soi-disant "deux différences" signifient qu'une fois l'objet itérable converti en itérateur, il perdra certains attributs (__getitem__). et ajoutez également quelques attributs (__next__).

Jetez d'abord un œil à l'attribut ajouté __next__. C'est la clé pour laquelle un itérateur est un itérateur. En fait, nous définissons un objet qui implémente à la fois la méthode __iter__ et la méthode __next__ en tant qu'itérateur de. .

Avec cet attribut supplémentaire, les objets itérables peuvent implémenter leur propre processus d'itération/traversement sans recourir à une syntaxe de boucle for externe. J'ai inventé deux concepts pour décrire ces deux processus de parcours (PS : pour faciliter la compréhension, cela s'appelle ici parcours, mais cela peut en fait être appelé itération) : le parcours fait référence au parcours implémenté via une grammaire externe, et l'auto-traversal fait référence au parcours implémenté grâce à ses propres méthodes.

Avec ces deux concepts, on dit qu'un objet itérable est un objet qui peut être "traversé par lui", et un itérateur est un objet qui peut aussi être "auto-traversé" sur cette base.

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

Comme le montre l'exemple ci-dessus, l'avantage de l'itérateur est qu'il prend en charge le parcours automatique. En même temps, il se caractérise par une non-boucle unidirectionnelle. terminée, une erreur sera signalée lors d’un nouvel appel.

À cet égard, j'ai pensé à une analogie : un objet itérable ordinaire est comme un chargeur de balles. Il traverse en retirant la balle et en la remettant après avoir terminé l'opération, afin qu'il puisse être parcouru à plusieurs reprises (c'est-à-dire que est, en appelant la boucle for plusieurs fois, renvoie le même résultat); et l'itérateur est comme un pistolet chargé d'un chargeur et qui ne peut pas être détaché lorsqu'il traverse ou se déplace automatiquement, il tire des balles. et ne peut pas être réutilisé (c'est-à-dire que le parcours aura une fin. ).

J'ai tellement écrit, permettez-moi de résumer un peu : L'itération est une manière de parcourir les éléments. Elle est divisée selon la méthode d'implémentation. Il existe deux types d'itération externe et d'itération interne. Objets prenant en charge l'itération externe (il traverse) C'est un objet itérable, et un objet qui prend également en charge l'itération interne (auto-traversée) est un itérateur selon la méthode de consommation, il peut être divisé en itération réutilisable et itération unique ; .Les objets itérables ordinaires sont réutilisables et les itérateurs L'appareil est jetable.

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.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer