【问题标题】:Creating a list within a list in Python在 Python 中的列表中创建列表
【发布时间】:2018-08-01 05:16:33
【问题描述】:

我有一个名为 values 的列表,其中包含一系列数字:

values = [0, 1, 2, 3, 4, 5, ... , 351, 0, 1, 2, 3, 4, 5, 6, ... , 750, 0, 1, 2, 3, 4, 5, ... , 559]

我想创建一个新列表,其中包含从 0 到数字的元素列表。

喜欢:

new_values = [[0, 1, 2, ... , 351], [0, 1, 2, ... , 750], [0, 1, 2, ... , 559]]

我做的代码是这样的:

start = 0
new_values = []
for i,val in enumerate(values): 
    if(val == 0):
        new_values.append(values[start:i]) 
        start = i

但是,它返回的是什么:

new_values = [[], [0, 1, 2, ... , 750], [0, 1, 2, ... , 559]]

如何修复我的代码?这将是一个很大的帮助。

【问题讨论】:

    标签: python list


    【解决方案1】:

    您可以根据 0 的存在(这是虚假的)将您的元素与 itertools.groupby 进行分组,并提取 0 之间的子列表,同时将缺少的 0 附加到列表理解中:

     [[0]+list(g) for k, g in groupby(values, bool) if k]
    

    例子:

    >>> from itertools import groupby
    >>> values = [0, 1, 2, 3, 4, 5 , 351, 0, 1, 2, 3, 4, 5, 6, 750, 0, 1, 2, 3, 4, 559]
    >>> [[0]+list(g) for k, g in groupby(values, bool) if k]
    [[0, 1, 2, 3, 4, 5, 351], [0, 1, 2, 3, 4, 5, 6, 750], [0, 1, 2, 3, 4, 559]]
    

    【讨论】:

    • 次要注意:这里假设0s 不会连续出现,并且values 总是以0 开头。 OP 的示例遵循此规则,因此如果保证该规则,这是迄今为止最简单/性能最高的解决方案。
    • 更正:It's actually quite slow as written 因为bool 有荒谬的开销。将bool 替换为operator.truth 作为key 函数将消除该开销,从而使成本与其他优化解决方案相似(稍慢,但微不足道,其中bool 可以使其花费优化非@ 的3 倍987654337@ 解决方案需要)。就个人而言,如果性能不重要,我会按原样使用此解决方案;如果性能很关键,切换到operator.truth 可以让您以最小的复杂性变化获得显着的加速。
    • 是的,我知道bool 比它应该的要慢,直到我与operator.truth 进行正面交锋时才意识到要慢多少。问题是 CPython 内置构造函数必须使用相当通用的调度机制,支持可变长度位置和关键字参数。解释器构建tupledict,然后booltp_new 调用PyArg_ParseTupleAndKeywords(出于某种愚蠢的原因,他们允许您实际使用bool 的关键字参数,这意味着他们需要检查他们每次);这只是构造函数簿记的基础知识。
    • 相比之下,operator.truth 是一个经过特别优化的普通函数类(它具有参数类型标志METH_O,这意味着它只需要一个位置参数,不多也不少,没有关键字args,其中tp_new 始终为METH_VARARGS | METH_KEYWORDS)。作为一个普通函数,没有调用特定于构造函数的机制;是METH_O,没有tupledict 必须创建并随后解包。最终,他们俩所做的工作是 PyBool_FromLong(PyObject_IsTrue(argval)),但它在 bool 构造函数中被埋没了。
    • 哦,嘿,显然 CPython 的人决定应该修复其中的一些决定; as of 3.7, bool no longer attempts to use keyword arguments,它只会抱怨是否传递了任何内容,并使用PyArg_UnpackTuple 解压缩tuple,而无需解析格式字符串,因此它应该运行得更快。没有 3.7 的副本可供测试,但它是一些东西。
    【解决方案2】:

    所以你写的代码的问题是它在开头包含一个空的list,并省略了最后的子list。对此的极简修复是:

    1. 更改测试以避免附加第一个 list(当 i 为 0 时),例如if val == 0 and i != 0:

    2. 在循环退出后追加最后一个组

    结合这两个修复,您将拥有:

    start = 0
    new_values = []
    for i,val in enumerate(values): 
        if val == 0 and i != 0:  # Avoid adding empty list
            new_values.append(values[start:i]) 
            start = i
    if values:  # Handle edgecase for empty values where nothing to add
        new_values.append(values[start:])  # Add final list
    

    我打算添加更清洁的groupby 解决方案,它可以避免list 的开头/结尾的特殊情况,但是Chris_Rands already handled that,所以我会向您推荐他的答案。

    有点令人惊讶的是,这实际上似乎是最快的解决方案,渐近地,以要求输入为list 为代价(其中一些其他解决方案可以接受任意迭代,包括不可能建立索引的纯迭代器)。

    为了比较(使用 Python 3.5 额外的解包泛化,既简洁又在现代 Python 上获得最佳性能,并使用 int 的隐式布尔值来避免与 0 进行比较,因为它等效于 int 输入,但是使用隐式布尔值更快):

    from itertools import *
    
    # truth is the same as bool, but unlike the bool constructor, it requires
    # exactly one positional argument, which makes a *major* difference
    # on runtime when it's in a hot code path
    from operator import truth
    
    def method1(values):
        # Optimized/correct OP's code
        # Only works on list inputs, and requires non-empty values to begin with 0,
        # but handles repeated 0s as separate groups properly
        new_values = []
        start = None
        for i, val in enumerate(values):
            if not val and i:
                new_values.append(values[start:i])
                start = i
        if values:
            new_values.append(values[start:])
        return new_values
    
    def method2(values):
        # Works with arbitrary iterables and iterators, but doesn't handle
        # repeated 0s or non-empty values that don't begin with 0
        return [[0, *g] for k, g in groupby(values, truth) if k]
    
    def method3(values):
        # Same behaviors and limitations as method1, but without verbose
        # special casing for begin and end
        start_indices = [i for i, val in enumerate(values) if not val]
    
        # End indices for all but terminal slice are previous start index
        # so make iterator and discard first value to pair properly
        end_indices = iter(start_indices)
        next(end_indices, None)
    
        # Pairing with zip_longest avoids need to explicitly pad end_indices
        return [values[s:e] for s, e in zip_longest(start_indices, end_indices)]
    
    def method4(values):
        # Requires any non-empty values to begin with 0
        # but otherwise handles runs of 0s and arbitrary iterables (including iterators)
        new_values = []
        for val in values:
            if not val:
                curlist = [val]
                new_values.append(curlist)
                # Use pre-bound method in local name for speed
                curlist_append = curlist.append
            else:
                curlist_append(val)
        return new_values
    
    def method5(values):
        # Most flexible solution; similar to method2, but handles all inputs, empty, non-empty,
        # with or without leading 0, with or without runs of repeated 0s
        new_values = []
        for nonzero, grp in groupby(values, truth):
            if nonzero:
                try:
                    new_values[-1] += grp
                except IndexError:
                    new_values.append([*grp])  # Only happens when values begins with nonzero
            else:
                new_values += [[0] for _ in grp]
        return new_values
    

    在 Python 3.6、Linux x64 上的计时,使用 ipython 6.1 的 %timeit 魔法:

    >>> values = [*range(100), *range(50), *range(150)]
    >>> %timeit -r5 method1(values)
    12.5 μs ± 50.6 ns per loop (mean ± std. dev. of 5 runs, 100000 loops each)
    
    >>> %timeit -r5 method2(values)
    16.9 μs ± 54.9 ns per loop (mean ± std. dev. of 5 runs, 100000 loops each)
    
    >>> %timeit -r5 method3(values)
    13 μs ± 18.9 ns per loop (mean ± std. dev. of 5 runs, 100000 loops each)
    
    >>> %timeit -r5 method4(values)
    16.7 μs ± 9.51 ns per loop (mean ± std. dev. of 5 runs, 100000 loops each)
    
    >>> %timeit -r5 method5(values)
    18.2 μs ± 25.2 ns per loop (mean ± std. dev. of 5 runs, 100000 loops each)
    

    总结:

    批量分割运行的解决方案method1method3最快,但取决于输入是一个序列(如果返回类型必须是list,输入也必须是list,或者必须添加转换)。

    groupby 解决方案 (method2, method5) 有点慢,但通常非常简洁(处理所有边缘情况,如method5不需要极端冗长,也不需要明确的测试和检查 LBYL 模式)。 除了使用operator.truth 而不是bool 之外,它们也不需要大量的黑客技术来使它们尽可能快地运行。这是必要的,因为 CPython 的 bool 构造函数 非常 很慢,这要归功于一些奇怪的实现细节(bool 必须接受完整的可变参数,包括关键字,通过对象构造机制进行调度,这比operator.truth 使用低开销路径,该路径仅采用一个位置参数并绕过对象构造机械);如果将bool 用作key 函数而不是operator.truth,则运行时间会增加一倍以上(method2method5 分别为 36.8 μs 和 38.8 μs)。

    介于两者之间的是更慢但更灵活的方法(处理任意输入可迭代对象,包括迭代器、处理没有特殊大小写的 0 运行等)逐项使用appends (method4)。问题是,获得最大性能需要更冗长的代码(因为需要避免重复索引和方法绑定);如果method4 的循环改得更简洁:

    for val in values:
        if not val:
            new_values.append([])
        new_values[-1].append(val)
    

    运行时间增加了一倍多(约 34.4 μs),这要归功于重复索引 new_values 和一遍又一遍地绑定 append 方法的成本。

    无论如何,就个人而言,如果性能不是绝对关键,我会使用bool 作为keygroupby 解决方案之一,以避免导入和不常见蜜蜂。 如果性能更重要,我可能仍然使用groupby,但将operator.truth 换成key 函数;当然,它没有拼写出来的版本那么快,但对于知道groupby 的人来说,它很容易上手,而且对于任何给定级别的边缘情况处理来说,它通常是最简洁的解决方案。

    【讨论】:

      【解决方案3】:

      您可以使用itertools.groupby,通过查找每个值小于在values 中进行它的元素的所有组:

      import itertools
      values = [0, 1, 2, 3, 4, 5, 351, 0, 1, 2, 3, 4, 5, 6, 750, 0, 1, 2, 3, 4, 5, 559]
      new_vals = [[i[-1] for i in b] for a, b in itertools.groupby(enumerate(values), key=lambda x:x[-1] <= values[x[0]+1] if x[0]+1 < len(values) else False)]
      final_data = [new_vals[i]+new_vals[i+1] for i in range(0, len(new_vals), 2)]
      

      输出:

      [[0, 1, 2, 3, 4, 5, 351], [0, 1, 2, 3, 4, 5, 6, 750], [0, 1, 2, 3, 4, 5, 559]]
      

      【讨论】:

      • 当您需要list 并依赖lambda 时使用map 只是比等效的listcomp 更慢/更冗长。只要您在请求下一个之前确定使用该组,您就不需要预先列出该组。您可以将 list(map(lambda x:x[-1], list(b))) 替换为 [x[-1] for x in b] 并且它的行为相同(除了更快,更易于阅读)。如果你真的喜欢map,从operator 导入itemgetter 并执行list(map(itemgetter(-1), b)) 至少可以避免速度下降,但我通常会坚持使用listcomp。
      • @ShadowRanger 列表理解肯定比map 好。请查看我最近的编辑。
      【解决方案4】:

      这应该可行:

      values = [0, 1, 2, 3, 4, 5, 351, 0, 1, 2, 3, 4, 5, 6, 750, 0, 1, 2, 3, 4, 5, 559]
      new_values = []
      
      split_at = 0  # split the list when this value is reached
      
      idx = -1
      for value in values:
          if value == split_at:
              idx += 1
              new_values.append([])
      
          new_values[idx].append(value)
      

      输出:

      [[0, 1, 2, 3, 4, 5, 351], [0, 1, 2, 3, 4, 5, 6, 750], [0, 1, 2, 3, 4, 5, 559]]
      

      它还处理边缘盒。

      我的方法比Chris_Rands's快一点,但也比Vasilis G's方法慢一点:

      from itertools import groupby
      
      
      values = [
          0, 1, 2, 3, 4, 5, 351,
          0, 1, 2, 3, 4, 5, 6, 750,
          0, 1, 2, 3, 4, 5, 559,
      ]
      
      
      def method1():
          new_values = []
      
          idx = -1
          for value in values:
              if value == 0:
                  idx += 1
                  new_values.append([])
      
              new_values[idx].append(value)
      
          return new_values
      
      
      def method2():
          new_values = [[0] + list(g) for k, g in groupby(values, bool) if k]
          return new_values
      
      
      def method3():
          indices = [index for index, value in enumerate(values) if value == 0] + [len(values)]
          new_values = [values[indices[i]:indices[i + 1]] for i in range(len(indices) - 1)]
          return new_values
      

      >>> timeit.timeit(method1, number=100000)
      0.6725746986698414
      >>> timeit.timeit(method2, number=100000)
      0.8143814620314903
      >>> timeit.timeit(method3, number=100000)
      0.6596001360341748
      

      【讨论】:

      • 您可以通过完全避免使用idx 来节省一些工作;你总是想在new_values 中追加到最后的list,所以只需使用负索引new_values[-1].append(...),而不是维护idx 并执行new_values[idx].append(...)
      • 或者,为了获得更快的性能,在您的 if 情况下,您可以:new_values.append([])add_to_cur_list = new_values[-1].append,然后将 if 之外的代码更改为 add_to_cur_list(value),这样您就可以避免完全常见的索引和方法绑定开销。
      • 另外,关于性能的说明:当要分组的运行变长时,my minimalist change to the OP's original code 实际上似乎是最快的(您的method3 代码紧随其后,尽管还有空间对它进行了一点改进,速度方面,特别是将 [values[indices[i]:indices[i + 1]] for i in range(len(indices) - 1)] 更改为 [values[s:e] for s, e in zip(indices, indices[1:])] 以删除两个显式索引查找和每个项目的添加)。尝试使用从 0 开始的组,每个组的长度约为 100 项。
      • 嗯。最后一点:我刚刚为更大的组编写了一组更全面的性能比较(这可以更好地了解解决方案的扩展方式,如果该方法每个项目更快,则避免惩罚中等固定设置成本的方法)。最令人惊讶的发现是使用bool 多少会减慢您的速度,因为key 功能会减慢您的速度。使用operator.truth(在这个用例中与bool做完全相同的工作)通过避免与对象构造机器相关的费用(最终甚至不构造任何东西,因为True/False是单身人士)。
      【解决方案5】:

      你也可以这样做:

      values = [0, 1, 2, 3, 4, 5, 351, 0, 1, 2, 3, 4, 5, 6, 750, 0, 1, 2, 3, 4, 5, 559]
      
      # Find all indices whose element is 0.
      indices = [index for index, value in enumerate(values) if value==0] + [len(values)]
      
      # Split the list accordingly
      values = [values[indices[i]:indices[i+1]] for i in range(len(indices)-1)]
      
      print(values)
      

      输出:

      [[0, 1, 2, 3, 4, 5, 351], [0, 1, 2, 3, 4, 5, 6, 750], [0, 1, 2, 3, 4, 5, 559]]
      

      【讨论】:

        猜你喜欢
        • 2018-10-12
        • 1970-01-01
        • 2023-02-04
        • 2021-05-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多