具有可变填充值的 Python itertools.zip_longest

Python itertools.zip_longest with mutable fillvalue

提问人:Wör Du Schnaffzig 提问时间:8/28/2023 最后编辑:Wör Du Schnaffzig 更新时间:8/28/2023 访问量:56

问:

在评估 Web 响应的代码中,我想从多个列表中压缩元素。但是,迭代器的元素是 的。 因此,我也想用 's 填充缺失值,但每个生成的元素都应该有自己的实例。dictdictdict

以下代码按 对每个列表中的元素进行分组。 只要指定了不可变,就没有问题。itertools.zip_longestfillvalue

import collections
import itertools

l1 = [{"a": 100}, {"b": 200}, {"c": 300}]
l2 = [{"d": 400}]

ll = list(itertools.zip_longest(l1, l2, fillvalue=0))
print(ll)

-> [({'a': 100}, {'d': 400}), ({'b': 200}, 0), ({'c': 300}, 0)]

现在,当指定一个可变对象时,所有 共享同一个实例,因此更改一个实例会更改所有实例:fillvaluefillvalue

import collections
import itertools

l1 = [{"a": 100}, {"b": 200}, {"c": 300}]
l2 = [{"d": 400}]

ll = list(itertools.zip_longest(l1, l2, fillvalue=dict()))
ll[1][1]["x"] = 150
print(ll)

-> [({'a': 100}, {'d': 400}), ({'b': 200}, {'x': 150}), ({'c': 300}, {'x': 150})]

为了防止所有字典共享相同的实例,我使用了:copy.deepcopy

import collections
import copy
import itertools

l1 = [{"a": 100}, {"b": 200}, {"c": 300}]
l2 = [{"d": 400}]

ll = list(itertools.zip_longest(l1, l2, fillvalue=copy.deepcopy(dict())))
ll[1][1]["x"] = 150
print(ll)

-> [({'a': 100}, {'d': 400}), ({'b': 200}, {'x': 150}), ({'c': 300}, {'x': 150})]

因此,仍然所有 的 共享同一个实例。dictfillvalue

我想补充一点,假设填充值为 。ll = [item or dict() for item in itertools.zip_longest(l1, l2)]None

那么,我怎样才能让每个都独一无二呢?fillvalue

python-3.x python-itertools 默认值 可变

评论


答:

2赞 ShadowRanger 8/28/2023 #1

你不能教你复制 s。您必须自己复制它们(这也将使原始值与 ),例如:zip_longestdictl2

ll = [(d1, d2.copy()) for d1, d2 in itertools.zip_longest(l1, l2, fillvalue={})]

或者只是按需从头开始制作它们(这将把空 s 与 分离,留下其余的别名,这可能是也可能不是你想要的):dictl2

ll = [(d1, d2 or {}) for d1, d2 in itertools.zip_longest(l1, l2)]

完美保留混叠的替代方案是更冗长的:

ll = [(d1, {} if d2 is None else d2) for d1, d2 in itertools.zip_longest(l1, l2)]

如果可能短于 ,则在每个 listcomp 中应用类似的转换。l1l2d1

评论

0赞 Wör Du Schnaffzig 8/28/2023
我认为这与@MadPhysicist的方法相同,但仅适用于两个源列表。这样做的优点是更容易掌握。然后可以将其推广。
0赞 ShadowRanger 8/29/2023
@WörDuSchnaffzig:是的,我们大约在同一时间发布,经过编辑(删除不必要的非默认值),我的解决方案 #3 最终与他们的解决方案基本相同,在这种情况下,并且保证不包含作为值(与 OP 的场景匹配),消除了对自定义哨兵的需求。就个人而言,我喜欢选项 #1 在许多情况下,因为它限制了 s in 之间的混叠和导致“远处的幽灵动作”的风险;您可以通过使用 As the expression to produce 来完全消除该风险。fillvaluel1l2Nonedictlll2copy.deepcopy((l1, l2))
0赞 ShadowRanger 8/29/2023
但是,如果目标是最小的开销,并且尽可能需要混叠,那么选项 #3 就是要走的路。选项 #2 拆分差异,执行部分锯齿(对于非空输入),以换取最简洁的代码(速度应该与 #3 相似,但如果真实性测试 [需要查看其长度] 最终比检查它慢,#3 可能会更快(这是一个严格的指针比较)。dictdictNone
3赞 Mad Physicist 8/28/2023 #2

可以使用 sentinel 值并包装在另一个管道中。zip_longest

例如:

sentinel = object()
l = list(tuple({} if x is sentinel else x for x in items) for items in zip_longest(l1, l2, fillvalue=sentinel))

对于此示例,您可能可以使用而不是自定义对象。Nonesentinel

评论

0赞 Wör Du Schnaffzig 8/28/2023
就我而言,我可以使用 的 sentinal ,这是 的默认值。所以我可以写:Nonefillvaluel = list(tuple(x if x is not None else {} for x in items) for items in zip_longest(l1, l2))
1赞 Mad Physicist 8/28/2023
@WörDuSchnaffzig。完全。如果可能的话,我更喜欢积极的条件,所以,但这只是个人审美{} if x is None else x
0赞 Wör Du Schnaffzig 8/28/2023
同意。应该看到这一点。
0赞 Kelly Bundy 8/28/2023 #3

享受更多迭代工具的乐趣,将 dict-producer 链接到列表中。这会将每个列表变成其元素的迭代器,然后是无限多的新空字典。压缩它们会得到所需的结果,但它不会在使用所有列表元素时停止。因此,我习惯于为我提供所需长度的迭代器,并使用该迭代器(in )来限制数据。zip_longestcompress

from itertools import starmap, chain, repeat, compress, zip_longest

l1 = [{"a": 100}, {"b": 200}, {"c": 300}]
l2 = [{"d": 400}]

lists = l1, l2
dicts = starmap(dict, repeat(()))
ll = list(compress(
    zip(*map(chain, lists, repeat(dicts))),
    zip_longest(*lists)
))

ll[1][1]['e'] = 500
print(ll)

输出(在线尝试!

[({'a': 100}, {'d': 400}),
 ({'b': 200}, {'e': 500}),
 ({'c': 300}, {})]

或者,用于在最大长度处停止:islice

ll = list(islice(
    zip(*map(chain, lists, repeat(dicts))),
    max(map(len, lists))
))

在线尝试!

评论

0赞 Wör Du Schnaffzig 8/28/2023
这看起来很详细,不容易理解。你能用几句话解释一下吗?
0赞 Kelly Bundy 8/28/2023
@WörDuSchnaffzig 添加了更多解释。
0赞 Kelly Bundy 8/28/2023
@WörDuSchnaffzig 并在最后添加了一个更简单的替代方案。(我习惯于使用迭代器,我没有奢侈地调用)。len()