为啥链式迭代如此复杂?简化此代码

Posted

技术标签:

【中文标题】为啥链式迭代如此复杂?简化此代码【英文标题】:Why is chaining iterables this complicated? Simplify this code为什么链式迭代如此复杂?简化此代码 【发布时间】:2015-02-11 04:17:27 【问题描述】:

我想链接多个可迭代对象,所有内容都使用惰性求值(速度至关重要),以执行以下操作:

从一大行标准输入中读取多个整数 split() 那一行 将生成的字符串转换为 int 计算连续整数之间的差异 ...以及此处未显示的其他内容

真实的例子比较复杂,这里有一个简化的例子:

这是标准输入的示例行: 2 13 4 16 16 15 22 17 8 8 7 6

(出于调试目的,下面的instream 可能指向 sys.stdin,或打开的文件句柄)

您不能简单地链接生成器,因为map() 返回一个(惰性求值的)列表:

import itertools
gen1 = map(int, (map(str.split, instream))) # CAN'T CHAIN DIRECTLY

我找到的最简单的工作解决方案是这个,它肯定不能简化吗?

gen1 = map(int, itertools.chain.from_iterable(itertools.chain(map(str.split, instream))))

为什么我需要链接 itertools.chain.from_iterable(itertools.chain 只是为了处理来自 map(str.split, instream) 的结果 - 这有点违背目的? 手动定义生成器是否更快?

【问题讨论】:

【参考方案1】:

显式(“手动”)生成器表达式应优先于使用mapfilter。它对大多数人来说更易读,也更灵活。

如果我理解你的问题,这个生成器表达式可以满足你的需要:

gen1 = ( int(x) for line in instream for x in line.split() )

【讨论】:

但我也说过我绝对需要惰性评估,形成line.split() 列表会减慢速度。以map(str.split, instream) 为起点。 @smci: map(str.split, instream) 形成与 line.split() 迭代时完全相同的列表; map 不能让 str.split 变得懒惰。 好的。你能回答一下为什么我需要那个蹩脚的itertools.chain.from_iterable(itertools.chain(... 吗? @smci: itertools.chain 有一个参数基本上什么都不做(它返回与其参数等效的东西)。简化为itertools.chain.from_iterable(...)【参考方案2】:

您可以手动构建您的生成器:

import string

def gen1(stream):
    # presuming that stream is of type io.TextIOBase


    s = ""
    c = stream.read(1)  
    while len(c)>0:

        if (c not in string.digits):
            if len(s) > 0:
                i = int(s)
                yield i
                s = ""
        else:
            s += c

        c = stream.read(1)

    if len(s) > 0:
        i = int(s)
        yield i 


import io
g = gen1(io.StringIO("12 45  6 7 88"))
for x in g:    # dangerous if stream is unlimited
    print(x)

这当然不是最漂亮的代码,但它可以满足您的需求。 说明:

如果您的输入无限长,您必须分块(或按字符)阅读。 每当您遇到非数字(空白)时,您都将在该点之前读取的字符转换为整数并生成它。 您还必须考虑到达 EOF 时会发生什么。 由于我正在按字符阅读,我的实现可能不是很好。使用块可以显着加快速度。

编辑为什么你的方法永远行不通:

map(str.split, instream)

根本不做你认为它做的事。 map 将给定函数 str.split 应用于作为第二个参数给出的迭代器的每个元素。在您的情况下,这是一个流,即一个文件对象,在 sys.stdin 的情况下特别是一个 io.TextIOBase 对象。这确实可以迭代。一行一行,这显然不是你想要的!实际上,您逐行遍历输入并将每一行拆分为单词。地图生成器迭代(许多)单词列表而不是单词列表。这就是为什么您必须将它们链接在一起以获得一个列表进行迭代。

另外,itertools.chain.from_iterable(itertools.chain(map(...))) 中的 itertools.chain() 是多余的。 itertools.chain 将其参数(每个都是不可更改的对象)链接到一个迭代器中。你只给它一个参数,所以没有任何东西可以链接在一起,它基本上返回地图对象不变。 另一方面,itertools.chain.from_iterable() 接受一个参数,该参数应为迭代器的迭代器(例如列表列表)并将其展平为一个迭代器(列表)。

EDIT2

import io, itertools

instream = io.StringIO("12 45 \n 66 7 88")
gen1 = itertools.chain.from_iterable(map(str.split, instream))
gen2 = map(int, gen1)
list(gen2)

返回

[12, 45, 66, 7, 88]

【讨论】:

我发布的代码确实工作:gen1 = map(int, itertools.chain.from_iterable(itertools.chain(map(str.split, instream)))) 不,itertools.chain.from_iterable(itertools.chain( 不是多余的;没有它,代码就会中断。试试看,自己看看。 @smci:是的,确实如此,但它并没有按照您描述的那样做。

以上是关于为啥链式迭代如此复杂?简化此代码的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C++ 语法如此复杂? [关闭]

为啥在 Vim 中将 Esc 重新映射到 CAPS LOCK 如此复杂?

《重构与模式》简化-积木系列

为啥此代码部分返回“分段错误”错误?

为啥要使用lambda表达式?原来如此,涨知识了

使用 Stream API 简化集合操作