Python生成器的生成器?

8
我写了一个类来读取文本文件。文件由非空行块(我们称之为“部分”)组成,用空行隔开:
line1.1
line1.2
line1.3

line2.1
line2.2

我的第一个实现是读取整个文件并返回一个列表的列表,也就是一系列的部分,每一部分又是由若干行组成的列表。但从内存的角度来看,这显然是很糟糕的。
因此,我将其重新实现为一个列表的生成器,也就是在每个周期中,我的类将整个部分作为一个列表读入内存,并将其生成。
这样做更好,但对于大型部分仍然存在问题。所以我想知道是否可以将其重新实现为一个生成器的生成器?问题是这个类非常通用,它应该能够满足以下两种用例:
1. 读取一个非常大的文件,其中包含非常大的部分,并且只循环一次。生成器的生成器非常适合这种情况。 2. 将一个小文件读入内存,以便多次循环。列表的生成器很好用,因为用户可以简单地调用 list(MyClass(file_handle))。
但是在第2种情况下,生成器的生成器不起作用,因为内部对象不会被转换为列表。
有没有比实现一个明确的 to_list() 方法更优雅的方法,让它将生成器的生成器转换为列表的列表?

你尝试过使用readline吗?这样只会读取一行数据,以换行符为界限。这是一种很好的方式来加载小规模的数据到内存中,除非你的行本身就很大。 - Vivek
@Vivek 我的代码行非常复杂,每一行都会生成一个对象来验证该行,并且其状态也取决于之前的行。将文件的内部格式暴露给用户不是一个选项。 - crusaderky
你能给一个样例输入行吗? - Vivek
1
问题到底是什么?如何编写生成器的生成器,还是如何为小文件创建列表,假设您有生成器?对于后一种情况:[list(section()) for section in MyClass(file_handle)]怎么样? - tobias_k
@tobias_k 这个问题是如何从生成器的生成器中创建列表的列表,以一种合理透明和优美的方式呈现给用户。您提供的示例是我如何实现我提到的显式to_list()方法,但我想知道是否有任何方法不需要用户调用显式的to_list()方法?换句话说,我希望避免库在分心的用户执行list(MyClass(file_handle))时神秘地崩溃。 - crusaderky
2个回答

8

Python 2:

map(list, generator_of_generators)

Python 3:

list(map(list, generator_of_generators))

或者两者都可以:
[list(gen) for gen in generator_of_generators]

由于生成的对象是“生成器函数”,而不仅仅是生成器,因此您需要执行以下操作。
[list(gen()) for gen in generator_of_generator_functions]

如果那样行不通,我不知道你在问什么。另外,为什么它会返回一个生成器函数而不是一个生成器本身?
由于您在评论中表示要避免list(generator_of_generator_functions)神秘崩溃,这取决于您真正想要的内容。
  • 无法以这种方式覆盖list的行为:您可以存储子生成器元素或不存储。

  • 如果您确实遇到崩溃,请建议每次主生成器迭代时使用主生成器循环耗尽子生成器。这是标准做法,也是itertools.groupby执行的操作,即标准库中的生成器。

例如:
def metagen():
    def innergen():
        yield 1
        yield 2
        yield 3

    for i in range(3):
        r = innergen()
        yield r

        for _ in r: pass
  • 或者使用一个黑暗的秘密方法,我稍后会展示(我需要写出来),但是不要这样做!

正如承诺的那样,这次提供的是 Python 3 的 hack 方法:

from collections import UserList
from functools import partial


def objectitemcaller(key):
    def inner(*args, **kwargs):
        try:
            return getattr(object, key)(*args, **kwargs)
        except AttributeError:
            return NotImplemented
    return inner


class Listable(UserList):
    def __init__(self, iterator):
        self.iterator = iterator
        self.iterated = False

    def __iter__(self):
        return self

    def __next__(self):
        self.iterated = True
        return next(self.iterator)

    def _to_list_hack(self):
        self.data = list(self)
        del self.iterated
        del self.iterator
        self.__class__ = UserList

for key in UserList.__dict__.keys() - Listable.__dict__.keys():
    if key not in ["__class__", "__dict__", "__module__", "__subclasshook__"]:
        setattr(Listable, key, objectitemcaller(key))


def metagen():
    def innergen():
        yield 1
        yield 2
        yield 3

    for i in range(3):
        r = Listable(innergen())
        yield r

        if not r.iterated:
            r._to_list_hack()

        else:
            for item in r: pass

for item in metagen():
    print(item)
    print(list(item))
#>>> <Listable object at 0x7f46e4a4b850>
#>>> [1, 2, 3]
#>>> <Listable object at 0x7f46e4a4b950>
#>>> [1, 2, 3]
#>>> <Listable object at 0x7f46e4a4b990>
#>>> [1, 2, 3]

list(metagen())
#>>> [[1, 2, 3], [1, 2, 3], [1, 2, 3]]

太糟糕了,我甚至不想解释。

关键是要有一个包装器,它可以检测是否已经迭代,如果没有,就运行 _to_list_hack,让人难以置信的是,它会修改 __class__ 属性。

由于布局冲突,我们必须使用 UserList 类并隐藏其所有方法,这只是另一层的笨重操作。

基本上,请不要使用这个 hack。你可以把它当作幽默来欣赏。


0
一种相当实用的方法是在创建“生成器生成器”时告诉它是否生成生成器或列表。虽然这不像让list神奇地知道该做什么那样方便,但它似乎比拥有特殊的to_list函数更舒适。
def gengen(n, listmode=False):
    for i in range(n):
        def gen():
            for k in range(i+1):
                yield k
        yield list(gen()) if listmode else gen()

根据listmode参数,这可以用来生成生成器或列表。

for gg in gengen(5, False):
    print gg, list(gg)
print list(gengen(5, True))

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