Python 如何通过键进行迭代器的完全外部联接/合并?

Python 如何通过键进行迭代器的完全外部联接/合并?,python,functional-programming,iterator,generator,Python,Functional Programming,Iterator,Generator,我有多个排序迭代器,它们生成键控数据,可通过列表表示: a = iter([(1, 'a'), (2, 't'), (4, 'c')]) b = iter([(1, 'a'), (3, 'g'), (4, 'g')]) 我想合并它们,使用键并跟踪哪个迭代器具有键的值。这应该相当于SQL中的完整外部联接: >>> list(full_outer_join(a, b, key=lambda x: x[0])) [(1, 'a', 'a'), (2, 't', None), (3,

我有多个排序迭代器,它们生成键控数据,可通过列表表示:

a = iter([(1, 'a'), (2, 't'), (4, 'c')])
b = iter([(1, 'a'), (3, 'g'), (4, 'g')])
我想合并它们,使用键并跟踪哪个迭代器具有键的值。这应该相当于SQL中的完整外部联接:

>>> list(full_outer_join(a, b, key=lambda x: x[0]))
[(1, 'a', 'a'), (2, 't', None), (3, None, 'g'), (4, 'c', 'g')]
我尝试使用
heapq.merge
itertools.groupby
,但是使用
merge
我已经丢失了有关迭代器的信息:

>>> list(heapq.merge(a, b, key=lambda x: x[0]))
[(1, 'a'), (1, 'a'), (2, 't'), (3, 'g'), (4, 'c'), (4, 'g')]
所以我可以用一个标签生成器

def tagged(it, tag):
    for item in it:
        yield (tag, *x)
合并标记的迭代器,按键分组,并使用标记创建dict:

merged = merge(tagged(a, 'a'), tagged(b, 'b'), key=lambda x: x[1])
grouped = groupby(merged, key=lambda x: x[1])
[(key, {g[0]: g[2] for g in group}) for key, group in grouped]
这给了我这个可用的输出:

[(1, {'a': 'a', 'b': 'a'}),
 (2, {'a': 't'}),
 (3, {'b': 'g'}),
 (4, {'a': 'c', 'b': 'g'})]
然而,我认为为每个组创建dict是相当昂贵的性能方面的,所以也许有一种更优雅的方法

编辑:

为了澄清这一点,数据集太大,无法放入内存,因此我肯定需要使用生成器/迭代器

编辑2:


为了进一步澄清,a和b应该只迭代一次,因为它们表示读取速度慢的大型文件。

这里有一个通过字典的解决方案。我在这里提供它,因为我不清楚字典在这种情况下是否效率低下

我相信可以用迭代器代替
dict\u of\u list
,但我在下面的解决方案中使用它进行演示

a = [(1, 'a'), (2, 't'), (4, 'c')]
b = [(1, 'a'), (3, 'g'), (4, 'g')]

dict_of_lists = {'a': a, 'b': b}

def gen_results(dict_of_lists):
    keys = {num for k, v in dict_of_lists.items() \
                for num, val in v}
    for key in keys:
        d = {k: val for k, v in dict_of_lists.items() \
                    for num, val in v if num == key}
        yield (key, d)
结果

list(gen_results(dict_of_lists))

[(1, {'a': 'a', 'b': 'a'}),
 (2, {'a': 't'}),
 (3, {'b': 'g'}),
 (4, {'a': 'c', 'b': 'g'})]

您可以使用
reduce
和函数中的生成器来更改
groupby
解决方案:

from itertools import groupby
from functools import reduce
def group_data(a, b):
   sorted_data = sorted(a+b, key=lambda x:x[0])
   data = [reduce(lambda x, y:(*x, y[-1]), list(b)) for _, b in groupby(sorted_data, key=lambda x:x[0])]
   current = iter(range(len(list(filter(lambda x:len(x) == 2, data)))))
   yield from [i if len(i) == 3 else (*i, None) if next(current)%2 == 0 else (i[0], None, i[-1]) for i in data]

print(list(group_data([(1, 'a'), (2, 't'), (4, 'c')], [(1, 'a'), (3, 'g'), (4, 'g')])))
输出:

[(1, 'a', 'a'), (2, 't', None), (3, None, 'g'), (4, 'c', 'g')]

“他试图避免每组都听写,”斯蒂芬诺赫表示同意。我添加了一条注释来反映这一事实。虽然这确实很方便,但它会将整个数据集加载到内存中。我正在寻找使用生成器的解决方案。@membranepotential,您的“可用输出”肯定需要在内存中使用全新的数据集吗?分组方法将增加额外的复杂性。为了显示输出,我需要在其中放入一个列表,但您可以将
[(key,{g[0]:g[2]表示组中的g})交换为key,group in grouped]
((key,{g[0]:g[2]表示组中的g})交换为key,group in grouped)这将是一个生成器表达式。这里有很好的创造力+1尽管有三个音符。(1) 最好让
group_data
接受数据作为参数,而不是硬编码数据值。(2) 在OP的示例中,
3
元组中的
None
是第一项,而在代码中,
None
是第二项。我不确定这是否是个问题,只是让你知道。(3) 你的代码有点难读。我建议将解决方案分解成更多的步骤。虽然这看起来确实很好,但这样我就失去了关于哪个迭代器提供了数据的信息(正如Christian在(2)中解释的)。使用普通reduce是我以前没有考虑过的事情,也许这是解决问题的关键。@membranepotential您能澄清一下如何获得
None
的位置吗?您的代码中并没有立即明确这一点。@ChristianDean谢谢!(1) 是固定的,我目前正在等待OP关于
None
的反馈。(3) 希望是一种改进。当然:元组的第一个位置应该有键,而在提供给group_data函数的位置应该有None或数据:因此结果类似于
[(a[0]或b[0],a[1]或None,b[1]或None)]
。另请参见备选输出。您是否解决了问题?请随意发布您自己的解决方案,或者接受以下解决方案之一(如果他们有帮助)。我有一个解决方案,但我还没有实施它,我会尽快发布它。干杯=)