使用itertools zip_longest,并将每个子列表的第一个项目作为填充值,而不是默认的None

7

我有一个列表的列表:

cont_det = [['TASU 117000 0', "TGHU 759933 - 0", 'CSQU3054383', 'BMOU 126 780-0', "HALU 2014 13 3"], ['40HS'], ['Ha2ardous Materials', 'Arm5 Maehinery']]

实际上,cont_det是一个巨大的列表,其中包含许多具有不规则长度的子列表。这只是一个演示样例。我想获得以下输出:
[['TASU 117000 0', '40HS', 'Ha2ardous Materials'], 
 ['TGHU 759933 - 0', '40HS', 'Arm5 Maehinery'], 
 ['CSQU3054383', '40HS', 'Ha2ardous Materials'], 
 ['BMOU 126 780-0', '40HS', 'Ha2ardous Materials'], 
 ['HALU 2014 13 3', '40HS', 'Ha2ardous Materials']]

这里的逻辑是对列表中的所有子列表进行zip_longest操作,但如果有任何一个子列表的长度小于所有子列表的最大长度(即第一个子列表的长度为5),则不使用默认值fillvalue=None,而是取该子列表的第一个项目作为填充值。例如在第二个子列表中,所有填充值都相同,在第三个子列表中,最后三个填充值由第一个值填充。
我使用以下代码得到了结果:
from itertools import zip_longest as zilo
from more_itertools import padded as pad
max_ = len(max(cont_det, key=len))
for i, cont_row in enumerate(cont_det):
    if len(cont_det)!=max_:
        cont_det[i] = list(pad(cont_row, cont_row[0], max_))
cont_det = list(map(list, list(zilo(*cont_det))))

这样做会得到我期望的结果。如果我使用list(zilo(*cont_det, fillvalue='')),那么就会得到这个结果:
[('TASU 117000 0', '40HS', 'Ha2ardous Materials'), 
 ('TGHU 759933 - 0', '', 'Arm5 Maehinery'), 
 ('CSQU3054383', '', ''), 
 ('BMOU 126 780-0', '', ''), 
 ('HALU 2014 13 3', '', '')]
< p>是否有任何其他方式(例如映射任何函数等)来填充zip_longest函数的参数fillvalue,以便我不必通过迭代列表来将每个子列表填充到比它之前最长的子列表长度,并且可以在只使用zip_longest一行完成此操作?


1
这是一个不错的难题。你的解决方案唯一可能的限制是需要提前知道长度,这使得zip_longest和pad变得毫无意义。除此之外,我很喜欢它。 - Mad Physicist
5个回答

2
您可以通过next查看每个迭代器,以提取第一个项目(“head”),然后创建标记迭代器结尾的sentinel对象,最后将所有内容chain在一起,方法如下:head -> remainder_of_iterator -> sentinel -> it.repeat(head)
这使用it.repeat来重放第一个项目,直到迭代器结束,因此我们需要引入一种方法来停止该过程,一旦最后一个迭代器命中其sentinel对象。为此,我们可以(滥用)利用map停止迭代,如果映射函数引发(或泄漏)StopIteration,例如从next调用已耗尽的迭代器。或者,我们可以使用带有两个参数的iter形式,在sentinel对象上停止(见下文)。
因此,我们可以将链接的迭代器映射到检查每个项目是否is sentinel的函数上,并执行以下步骤:
  1. if item is sentinel,则通过next消耗一个专用迭代器,该迭代器产生比总迭代器数量少一个项目(因此泄漏了最后一个sentinel的StopIteration),并将sentinel替换为相应的head
  2. else只需返回原始项目。
最后,我们只需zip迭代器在一起-它将在最后一个命中其sentinel对象时停止,即执行“zip-longest”。
总之,以下函数执行上述步骤:
import itertools as it


def solution(*iterables):
    iterators = [iter(i) for i in iterables]  # make sure we're operating on iterators
    heads = [next(i) for i in iterators]  # requires each of the iterables to be non-empty
    sentinel = object()
    iterators = [it.chain((head,), iterator, (sentinel,), it.repeat(head))
                 for iterator, head in zip(iterators, heads)]
    # Create a dedicated iterator object that will be consumed each time a 'sentinel' object is found.
    # For the sentinel corresponding to the last iterator in 'iterators' this will leak a StopIteration.
    running = it.repeat(None, len(iterators) - 1)
    iterators = [map(lambda x, h: next(running) or h if x is sentinel else x,  # StopIteration causes the map to stop iterating
                     iterator, it.repeat(head))
                 for iterator, head in zip(iterators, heads)]
    return zip(*iterators)

如果从映射函数中泄漏StopIteration以终止map迭代器感觉太笨拙,那么我们可以稍微修改running的定义,以产生一个额外的sentinel并使用iter的2个参数形式来停止sentinel

running = it.chain(it.repeat(None, len(iterators) - 1), (sentinel,))
iterators = [...]  # here the conversion to map objects remains unchanged
return zip(*[iter(i.__next__, sentinel) for i in iterators])

如果在映射的函数内部,对于 sentinelrunning 的名称解析是一个问题,那么它们可以作为该函数的参数包含进去:

iterators = [map(lambda x, h, s, r: next(r) or h if x is s else x,
                 iterator, it.repeat(head), it.repeat(sentinel), it.repeat(running))
             for iterator, head in zip(iterators, heads)]

1

看起来像是某种“矩阵旋转”。

我已经完成了它,而没有使用任何库,以便让每个人都能明白。对我来说相当容易。

from pprint import pprint

cont_det = [
    ['TASU 117000 0', "TGHU 759933 - 0", 'CSQU3054383', 'BMOU 126 780-0', "HALU 2014 13 3"],
    ['40HS'],
    ['Ha2ardous Materials', 'Arm5 Maehinery'],
]


def rotate_matrix(source):
    result = []

    # let's find the longest sub-list length
    length = max((len(row) for row in source))

    # for every column in sub-lists create a new row in the resulting list
    for column_id in range(0, length):
        result.append([])

        # let's fill the new created row using source row columns data.
        for row_id in range(0, len(source)):
            # let's use the first value from the sublist values if source row list has it for the column_id
            if len(source[row_id]) > column_id:
                result[column_id].append(source[row_id][column_id])
            else:
                try:
                    result[column_id].append(source[row_id][0])
                except IndexError:
                    result[column_id].append(None)

    return result


pprint(rotate_matrix(cont_det))

当然,脚本输出。

> python test123.py
[['TASU 117000 0', '40HS', 'Ha2ardous Materials'],
 ['TGHU 759933 - 0', '40HS', 'Arm5 Maehinery'],
 ['CSQU3054383', '40HS', 'Ha2ardous Materials'],
 ['BMOU 126 780-0', '40HS', 'Ha2ardous Materials'],
 ['HALU 2014 13 3', '40HS', 'Ha2ardous Materials']]

无法理解zip_longest函数。它是解决方案的要求还是你需要一个“只能工作”的解决方案 :) 因为它似乎不支持任何回调函数或等等,我们可以在矩阵中“每个单元格”返回所需值。

1
zip_longest 不是必需的,只有当我们可以比我已经尝试过的更短地完成整个任务时(它已经可以工作了)。尽管如此,您的代码运行良好并提供所需的输出。 - Arkistarvh Kltzuonstev
是的,我喜欢“易于阅读”的解决方案,而不是那些复杂的一行代码,如果在写完几天后没有查看Python文档就无法阅读。 :D - Alexandr Shurigin

1
如果您想以一般方式处理任意迭代器,可以将一个哨兵值作为默认值,并用该列的第一个值替换它。这样做的优点是,它可以在不需要预先扩展或知道长度的情况下工作。
def zip_longest_special(*iterables):
    def filter(items, defaults):
        return tuple(d if i is sentinel else i for i, d in zip(items, defaults))
    sentinel = object()
    iterables = zip_longest(*iterables, fillvalue=sentinel)
    first = next(iterables)
    yield filter(first, [None] * len(first))
    for item in iterables:
        yield filter(item, first)

0

添加另一种变体

def zipzag(fill, *cols):
   
   sizes = [len(col) for col in cols] # size of individual list in nested list
   
   longest = max(*sizes) 
   
   return [[xs[i] if i < sizes[j] else fill(xs) for j, xs in enumerate(cols)]for i in range(longest)] 

cont_det = [['TASU 117000 0', "TGHU 759933 - 0", 'CSQU3054383', 'BMOU 126 780-0', "HALU 2014 13 3"], ['40HS'], ['Ha2ardous Materials', 'Arm5 Maehinery']] 
                           

print(zipzag(lambda xs: xs[0], *cont_det))                    

生成,

[['TASU 117000 0', '40HS', 'Ha2ardous Materials'], ['TGHU 759933 - 0', '40HS', 'Arm5 Maehinery'], ['CSQU3054383', '40HS', 'Ha2ardous Materials'], ['BMOU 126 780-0', '40HS', 'Ha2ardous Materials'], ['HALU 2014 13 3', '40HS', 'Ha2ardous Materials']]

[Program finished]

fill函数接收一个列表,并应返回某些内容以使列表的长度匹配并使zip工作。我给出的示例返回列的第一个元素


0

答案是否定的。对于fillvalue参数只有一个含义。无论如何,这里曾经有另一个相当不错的答案,但突然被删除了。下面的代码与那段代码非常接近,但它使用的是itertools而不是列表方法。

from itertools import chain, repeat
def zilo(data):
    try:
        i1 = next(it := iter(data))
    except StopIteration:
        return zip()
    return zip(chain(i1, repeat(i1[0], len(max(data, key=len))-len(i1))),
               *(chain(i, repeat(i[0])) for i in it))

这将是针对Python 3.8版本的,使用那个海象运算符! - Arkistarvh Kltzuonstev
@Arkistarvh Kltzuonstev 当然可以。但是替换它有什么问题吗? - facehugger

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