이 글은 Python의 반복자와 반복자 슬라이스에 대해 자세히 소개합니다. 필요한 친구들이 참고할 수 있기를 바랍니다.
Python 슬라이싱에 관한 처음 두 기사에서 우리는 기본 사용법, 고급 사용법, 슬라이싱에 대한 오해, 사용자 정의 객체가 슬라이싱 사용법을 구현하는 방법을 배웠습니다(관련 내용은 기사 끝 부분 참조). 링크) ). 이번 글은 슬라이싱 시리즈의 세 번째 글이며, 주요 내용은 이터레이터 슬라이싱이다.
Iterator는 Python의 고유한 고급 기능이며, 슬라이싱도 고급 기능입니다. 이 둘을 결합하면 어떤 결과가 나올까요?
우선, 명확히 해야 할 몇 가지 기본 개념이 있습니다: 반복, 반복 가능한 객체 및 반복자.
반복
은 컨테이너 유형 객체(예: 문자열, 목록, 사전 등)를 순회하는 방법입니다. 예를 들어 문자열 "abc"를 반복한다고 합니다. 수단은 모든 문자를 왼쪽에서 오른쪽으로 하나씩 꺼내는 과정입니다. (PS: 중국어로 iteration이라는 단어는 반복되는 주기와 레이어별 진행을 의미하지만, Python에서는 이 단어를 迭代
是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“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__()
One-way 수평 선형
# 方法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>for 루프는 반복 프로세스를 구현할 수 있지만 모든 객체를 for 루프에서 사용할 수 있는 것은 아닙니다. 예를 들어 위의 예에서 문자열 "abc"가 정수로 대체되는 경우입니다. , 오류: 'int' 객체는 반복 가능하지 않습니다.이 오류에서 "iterable"이라는 단어는 "iterable"을 나타냅니다. 즉, int 유형은 반복 가능하지 않습니다. 문자열 유형은 반복 가능하며 마찬가지로 목록, 튜플, 사전과 같은 유형도 모두 반복 가능합니다. 그러면 객체가 반복 가능한지 여부를 어떻게 판단할 수 있을까요? 왜 반복 가능합니까? 객체를 반복 가능하게 만드는 방법은 무엇입니까?
객체를 반복 가능하게 만들려면 반복 가능 프로토콜을 구현해야 합니다. 즉, __iter__()
매직 메서드를 구현해야 합니다. 이 매직 메소드를 구현하는 객체는 Iterable 객체입니다. 그럼 객체가 이 메서드를 구현하는지 여부를 어떻게 판단할 수 있을까요? 위의 for 루프 외에도 네 가지 다른 메서드가 있다는 것을 알고 있습니다:
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
이러한 메서드 중 가장 주목할만한 메서드는 Python의 내장 메서드인 iter() 메서드와 해당 함수입니다.
반복 가능한 객체를 반복자로 변환입니다. 이 문장은 두 가지 의미로 해석될 수 있습니다. (1) 반복 가능한 객체와 반복자는 서로 다른 것입니다. (2) 반복 가능한 객체는 반복자가 될 수 있습니다.
사실 반복자는 반복 가능한 객체여야 하지만 반복 가능한 객체가 반드시 반복자는 아닙니다. 둘 사이의 차이는 얼마나 큽니까?
위 그림의 파란색 원에 표시된 것처럼 일반 반복 가능 객체와 반복자 간의 가장 중요한 차이점은 다음과 같습니다. 소위 "동일하다"는 것은 둘 다 반복 가능하다는 것을 의미합니다(__iter__). 소위 "두 개의 서로 다르다"는 것은 반복 가능한 객체가 반복자로 변환된 후 일부 속성(__getitem__)을 잃게 됨을 의미합니다. 일부 속성(__next__)을 늘립니다.추가된 속성 __next__을 살펴보세요. 이것이 반복자가 반복자인 이유에 대한 핵심입니다. 실제로 우리는 __iter__ 메서드와 __next__ 메서드를 모두 구현하는 객체를 반복자로 정의합니다. .
이 추가 속성을 사용하면 반복 가능한 객체는 외부 for 루프 구문을 사용하지 않고도 자체 반복/순회 프로세스를 구현할 수 있습니다. 나는 이 두 가지 순회 프로세스를 설명하기 위해 두 가지 개념을 고안했습니다. (PS: 이해의 편의를 위해 여기서는 순회라고 부르지만 실제로는 반복이라고 부를 수도 있습니다.) 순회는 외부 문법을 통해 구현된 순회를 의미하고, 자체 순회는 구현된 순회를 의미합니다. 자신만의 방법을 통해서. #🎜🎜##🎜🎜#이 두 가지 개념의 도움으로 반복 가능한 객체는 "그것에 의해 탐색"될 수 있는 객체이고 반복자는 "자체 탐색"될 수도 있는 객체라고 말합니다. 이 기초. #🎜🎜#hi = "欢迎关注公众号:Python猫" it = iter(hi) # 普通切片 hi[-7:] # Python猫 # 反例:迭代器切片 it[-7:] # 报错:'str_iterator' object is not subscriptable#🎜🎜#위의 예에서 볼 수 있듯이 반복자의 장점은 자체 순회를 지원하는 동시에 일단 순회가 이루어지지 않는 것이 특징입니다. 완료되면 다시 호출하면 오류가 보고됩니다. #🎜🎜##🎜🎜#이런 점에서 비유를 생각해 봤습니다. 일반적인 반복 가능 개체는 총알을 꺼냈다가 작업 완료 후 다시 넣는 방식으로 이동하므로 반복적으로 이동할 수 있습니다. (즉, 여러 번 호출하여 동일한 결과를 반환함) 반복자는 탄창이 장착된 총과 같으며 이동하거나 자체 이동하면 총알을 발사합니다. 재사용할 수 없습니다(즉, 순회가 끝납니다). #🎜🎜##🎜🎜# 너무 많이 썼는데 조금 요약하자면: #🎜🎜# 반복은 요소를 순회하는 방식으로 구현 방법에 따라 두 가지 유형이 있습니다. 내부 반복을 지원하는 객체는 반복 가능한 객체이고, 내부 반복(자체 탐색)도 지원하는 객체는 사용 방법에 따라 재사용 가능한 반복과 일회성 반복으로 나눌 수 있습니다. . 일반적인 반복 가능 객체는 재사용이 가능하지만 반복자는 일회용입니다. #🎜🎜##🎜🎜#
前面提到了“一同两不同”,最后的不同是,普通可迭代对象在转化成迭代器的过程中会丢失一些属性,其中关键的属性是 __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 중국어 웹사이트의 기타 관련 기사를 참조하세요!