如何切片生成器对象或迭代器?

33

我想循环遍历一个迭代器的“片段”。我不确定是否可能,因为我了解到无法对迭代器进行切片。我想做的是这样的:

def f():
    for i in range(100):
        yield(i)
x = f()

for i in x[95:]:
    print(i)

当然,这会失败:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-37-15f166d16ed2> in <module>()
  4 x = f()
  5 
----> 6 for i in x[95:]:
  7     print(i)

TypeError: 'generator' object is not subscriptable

有没有一种Pythonic的方法可以循环遍历生成器的“切片”?

基本上,我关心的生成器读取一个非常大的文件,并逐行执行一些操作。我想测试文件的片段以确保一切都按预期执行,但让它运行整个文件非常耗时。

编辑:
如上所述,我需要在文件上执行此操作。我希望有一种明确指定生成器的方式,例如:

import skbio

f = 'seqs.fna'
seqs = skbio.io.read(f, format='fasta')

seqs是一个生成器对象

for seq in itertools.islice(seqs, 30516420, 30516432):
    #do a bunch of stuff here
    pass

上述代码已经能够满足我的需求,但是由于生成器仍然需要遍历所有行,因此速度仍然非常慢。我希望只遍历指定的切片。


我不明白你的问题... 如果你的生成器以文件作为输入,那么要测试它,就传递该文件的片段,你为什么想要“切割生成器”? - xrisk
6
你有没有查看过itertools.islice?它是Python中的一个库函数,用于对迭代器进行切片操作。 - jonrsharpe
4
请注意,对生成器进行islice操作不能阻止它继续处理在您关心的行之前的行。最好为其提供文件的一个islice。(您仍需要读取文件以查找换行符,但会跳过生成器在不需要的行上执行的任何处理。) - user2357112
4个回答

34

一般来说,答案是 itertools.islice,但你需要注意的是,islice 实际上不能跳过值,它只是在开始 yield 值之前抓取并丢弃 start 个值。因此,在需要跳过大量值和/或跳过的值很难获取/计算时,通常最好避免使用 islice。如果可以找到不生成这些值的方法,请这样做。在你(明显是虚构的)的例子中,你只需调整 range 对象的起始索引。

特别是当尝试在文件对象上运行时,拉取大量行(特别是从较慢的介质读取)可能并不理想。假设你不需要特定的行,那么你可以使用一个技巧来避免实际读取整个文件块,同时仍然测试文件中的某些距离,即 seek 到猜测的偏移量,读取到行末(以丢弃你可能进入中间的部分行),然后从那个点开始使用 islice 来选取任意多行。例如:

import itertools

with open('myhugefile') as f:
    # Assuming roughly 80 characters per line, this seeks to somewhere roughly
    # around the 100,000th line without reading in the data preceding it
    f.seek(80 * 100000)
    next(f)  # Throw away the partial line you probably landed in the middle of
    for line in itertools.islice(f, 100):  # Process 100 lines
        # Do stuff with each line

针对文件的特定情况,您可能还需要查看mmap,它可以以类似的方式使用(如果您正在处理数据块而不是文本行,并且可能随机跳转时非常有用)。

更新:根据您更新后的问题,您需要查看API文档和/或数据格式来确定如何正确地跳过。看起来skbio提供了一些使用seq_num进行跳过的功能,但这仍将读取文件中的大部分内容。 如果数据是按相等的序列长度写出的,我会查看Alignment上的文档;对齐的数据可能可以在完全不处理前面的数据的情况下加载,例如通过使用Alignment.subalignment创建新的Alignment,以便为您跳过其余的数据


1
在一个未结构化、未索引的文件中,有没有办法获取第100,000行(确切地)而不必遍历整个文件? - Nick T
1
@NickT:不是的。像linecache这样的模块可以让你假装你有随机访问,但它仍然会“翻阅”整个文件;没有任何有意义的方法可以在不读取文件的情况下找到换行符的位置。映射文件并重复使用mmap.findmmap.rfind可以相对于文件的开头或结尾找到行,而无需存储任何行在内存中,但它仍然需要读取文件。 - ShadowRanger
1
@NickT:我之前曾经发布过一个使用mmap读取大文件最后X行而不需要一次性读取整个文件的答案;那是你能得到的最接近的答案。你需要从文件的一端或另一端读取,如果行没有固定长度,你不能跳转到给定的行而不读取以确定该特定行在哪里。 - ShadowRanger

6

islice是Pythonic的方式

from itertools import islice    

g = (i for i in range(100))

for num in islice(g, 95, None):
    print num

我知道这和问题无关,但为什么不用g = list(range(100))代替你的第二行呢? - undefined
@HosseinGholami 你提出的更改将导致 g 成为一个列表,而不是一个迭代器。 - undefined

5
你无法使用普通的切片操作来切割生成器对象或迭代器。相反,你需要使用itertools.islice,就像@jonrsharpe在他的评论中提到的那样。
import itertools    

for i in itertools.islice(x, 95)
    print(i)

注意,islice返回一个迭代器并消耗迭代器或生成器上的数据。因此,如果您需要返回并执行某些操作或使用鲜为人知的itertools.tee创建生成器的副本,则需要将数据转换为列表或创建新的生成器对象。
from itertools import tee


first, second = tee(f())

注意:itertools.tee 存储了最先进的 tee 副本产生的每个输出的副本,并且在最不先进的迭代器产生它之前不能丢弃任何这些值。因此,在读取第二个迭代器之前耗尽一个 tee 副本的使用通常最好通过将原始生成器转换为列表,然后多次迭代来处理。 - ShadowRanger
@ShadowRanger 你的意思是通过迭代原始数据,副本也会被消耗吗?能否请您详细说明一下?将原始生成器转换为列表意味着要将所有数据加载到内存中。 - styvane
我从未提到过迭代原始副本; 不确定你的意思是什么?基本上,如果你做 x,y = tee(some_generator_making_numbers),然后做 sum(x),那么 some_generator_making_numbers 的所有值都存储在 tee 共享数据中,直到你也从 y 中耗尽它们; 如果你不几乎同时迭代 tee 的所有输出,那么你不太可能减少内存开销,而只是使用 somelist = list(some_generator_making_numbers) 将其转换为列表,然后按照需要多次迭代 somelist - ShadowRanger
3
重点是,tee实际上并没有复制生成器。它基于一个共享缓存创建新的生成器,在此缓存中,第一个请求项目X的生成器导致共享缓存从原始生成器中提取该值,并且在最后一个请求项目X的生成器释放该值时将其释放。但是,如果第一个tee生成器在第二个生成器甚至拉取单个值之前运行到耗尽,则共享缓存包含来自原始生成器的每个值(所需内存大约相当于已存储所有值的列表)。 - ShadowRanger
1
这实际上是 tee 文档 的一部分: "这个迭代工具可能需要大量的辅助存储空间(取决于需要存储多少临时数据)。通常情况下,如果一个迭代器在另一个迭代器开始之前使用了大部分或全部数据,则使用 list() 比使用 tee() 更快。" - ShadowRanger

-1

首先让我们澄清一些事情。如果您想从生成器中提取前几个值,那么就需要在表达式左侧指定参数数量。但是,这时候我们会遇到一个问题,因为在Python中有两种解包的方式。

让我们通过以下示例来讨论这两种方式。假设您有以下列表l = [1, 2, 3]

1)第一种方式是不使用“start”表达式

a, b, c = l # a=1, b=2, c=3

如果左侧表达式的参数数量(在本例中为3个参数)等于列表中元素的数量,则此方法非常有效。但是,如果您尝试类似以下代码:

a, b = l # ValueError: too many values to unpack (expected 2)

出现这种情况是因为列表包含的参数比表达式左侧指定的参数更多。

2)第二种选择是使用“start”表达式;这样可以解决上一个错误。

a, b, c* = l #  a=1, b=2, c=[3]

"start"参数的作用类似于缓冲列表。 缓冲区可能有三种可能的值:

    a, b, *c = [1, 2] # a=1, b=2, c=[]
    a, b, *c = [1, 2, 3] # a=1, b=2, c=[3]
    a, b, *c = [1, 2, 3, 4, 5] # a=1, b=2, c=[3,4,5]

请注意,列表必须至少包含两个值(在上面的示例中)。如果没有,则会引发错误。
现在,转到你的问题。如果您尝试像这样做:
a, b, c = generator

只有当生成器仅包含三个值时才能正常工作(生成器值的数量必须与左侧参数的数量相同)。否则,将会引发错误。

如果您尝试类似以下的操作:

a, b, *c = generator
  • 如果生成器中的值少于2个,则会引发错误,因为变量"a"、"b"必须有一个值
  • 如果生成器中的值为3,则a=,b=(val_2>,c=[]
  • 如果生成器中的值大于3,则a=,b=(val_2>,c=[, ... ] 在这种情况下,如果生成器是无限的,则程序将被阻塞,试图消耗生成器

我为您提出的解决方案如下

# Create a dummy generator for this example
def my_generator():
i = 0
while i < 2:
    yield i
    i += 1

# Our Generator Unpacker
class GeneratorUnpacker:
    def __init__(self, generator):
        self.generator = generator

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self.generator)
        except StopIteration:
            return None # When the generator ends; we will return None as value

if __name__ == '__main__':
    dummy_generator = my_generator()
    g =  GeneratorUnpacker(dummy_generator )
    a, b, c = next(g), next(g), next(g)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接