【问题标题】:Python list comprehensions to create multiple lists [duplicate]Python列表推导创建多个列表[重复]
【发布时间】:2014-01-28 04:42:46
【问题描述】:

我想创建两个列表 listOfAlistOfB 来存储来自另一个列表的 AB 的索引。

s=['A','B','A','A','A','B','B']

输出应该是两个列表

listOfA=[0,2,3,4]
listOfB=[1,5,6]

我可以用两条语句做到这一点。

listOfA=[idx for idx,x in enumerate(s) if x=='A']
listOfB=[idx for idx,x in enumerate(s) if x=='B']

但是,我只想使用列表推导在一次迭代中完成。 是否可以在单个语句中完成? 类似listOfA,listOfB=[--code goes here--]

【问题讨论】:

  • @kojiro:这里没有复杂性不是问题,我只是想探索一下python的特性。
  • 由于问题已关闭,将在此处添加答案:s = ['A','B','A','A','A','B','B']listOfA, listOfB = [], [][listOfA.append(c) if c == 'A' else listOfB.append(c) for c in s]

标签: python list list-comprehension


【解决方案1】:

列表推导式的定义是生成一个列表对象。您的 2 个列表对象甚至具有不同的长度;你必须使用副作用来实现你想要的。

这里不要使用列表推导。只需使用普通循环:

listOfA, listOfB = [], []

for idx, x in enumerate(s):
    target = listOfA if x == 'A' else listOfB
    target.append(idx)

这让您只需要执行 一个 循环;这将击败任何两个列表推导,至少在开发人员找到一种方法使列表推导构建一个列表的速度是使用单独的 list.append() 调用的循环之前的两倍。

我会在任何一天都选择这个而不是嵌套列表理解只是以便能够在一行上生成两个列表。正如Zen of Python 所说:

可读性很重要。

【讨论】:

  • 列表理解(生成单个列表)是否比通过追加生成列表更快?
  • @Heisenberg:是的,因为那时 Python 可以完全在 C 中构建列表。没有讨厌的 Python 堆栈推送和弹出,没有.append() 属性查找。我们可以稍微优化后者(在循环外使用 A, B = listOfA.append, listOfB.append 并重用它们),但堆栈调用仍然会比 C 代码慢。
【解决方案2】:

有点;关键是生成一个 2 元素列表,然后您可以解包:

listOfA, listOfB = [[idx for idx, x in enumerate(s) if x == c] for c in 'AB']

也就是说,我认为这样做很愚蠢,显式循环更具可读性。

【讨论】:

  • 这仍然循环两次,并且非常不可读。
【解决方案3】:

解决这个问题的一个好方法是使用 defaultdict。正如@Martin 已经说过的,列表理解不是生成两个列表的正确工具。使用 defaultdict 将使您能够使用单次迭代创建隔离。此外,您的代码不会受到任何形式的限制。

>>> from collections import defaultdict
>>> s=['A','B','A','A','A','B','B']
>>> listOf = defaultdict(list)
>>> for idx, elem in enumerate(s):
    listOf[elem].append(idx)
>>> listOf['A'], listOf['B']
([0, 2, 3, 4], [1, 5, 6])

【讨论】:

  • 对于两个键,我会在条件语句上投入资金,胜过您的 hash(elem) 电话。
  • @MartijnPieters:我不会和你打赌。我只是提供一个替代方案,前提是 OP 想要将这个想法扩展到多个键(项目)。
【解决方案4】:

您尝试做的事情并非完全不可能,只是很复杂,而且可能很浪费。

如果您想将一个可迭代对象划分为两个可迭代对象,并且源是一个列表或其他可重复使用的可迭代对象,那么您最好分两遍进行,就像您的问题一样。

即使源是一个迭代器,如果你想要的输出是一对列表,而不是一对惰性迭代器,要么使用Martijn's answer,要么对list(iterator)进行两次传递。)

但是,如果您确实需要将任意可迭代对象延迟划分为两个可迭代对象,那么如果没有某种中间存储,就无法做到这一点。

假设您将[1, 2, -1, 3, 4, -2] 划分为positivesnegatives。现在你尝试next(negatives)。那应该给你-1,对吧?但是如果不使用12,它就无法做到这一点。这意味着当您尝试next(positives) 时,您将获得3 而不是1。因此,12 需要存储在某个地方。

您需要的大部分聪明才智都包含在 itertools.tee 中。如果你只是将 positivesnegatives 制作成同一个迭代器的两个 teed 副本,然后过滤它们,你就完成了。

事实上,这是itertools 文档中的配方之一:

def partition(pred, iterable):
    'Use a predicate to partition entries into false entries and true entries'
    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
    t1, t2 = tee(iterable)
    return filterfalse(pred, t1), filter(pred, t2)

(如果您不能理解这一点,可能值得明确地写出来,两个生成器函数通过闭包共享一个迭代器和一个 tee,或者一个类的两个方法通过 self 共享它们。它应该是几十行不需要任何棘手的代码。)

您甚至可以从 more_itertools 等第三方库导入 partition


现在,您可以在单行中使用它:

lst = [1, 2, -1, 3, 4, -2]
positives, negatives = partition(lst, lambda x: x>=0)

... 你有一个遍历所有正值的迭代器和一个遍历所有负值的迭代器。它们看起来像是完全独立的,但它们一起只对lst 进行一次传递——因此即使您将lst 分配给生成器表达式或文件或其他东西而不是列表,它也可以工作。


那么,为什么没有某种快捷语法呢?因为这会很误导人。

推导不需要额外的存储空间。这就是生成器表达式如此出色的原因——它们可以将一个惰性迭代器转换为另一个惰性迭代器,而无需存储任何内容。

但这需要O(N) 存储。想象一下所有的数字都是正数,但你首先尝试迭代negative。发生什么了?所有号码都被推送到trueq。事实上,O(N) 甚至可能是无限(例如,在 itertools.count() 上试试)。

这对于itertools.tee 之类的东西很好,这是一个卡在模块中的函数,大多数新手甚至都不知道,它有很好的文档可以解释它的作用并明确成本。但是如果使用语法糖让它看起来像正常的理解,那就是另一回事了。

【讨论】:

    【解决方案5】:

    对于那些生活在边缘的人 ;)

    listOfA, listOfB = [[i for i in cur_list if i is not None] for cur_list in zip(*[(idx,None) if value == 'A' else (None,idx) for idx,value in enumerate(s)])]
    

    【讨论】:

      猜你喜欢
      • 2018-06-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-20
      • 2018-07-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多