我该如何基于条件来分割(拆分、划分)一个列表?

383

我有一些代码,类似于:

good = [x for x in mylist if x in goodvals]
bad = [x for x in mylist if x not in goodvals]

目标是根据条件将mylist的内容拆分成另外两个列表。

我该如何更加优雅地完成这个操作?能否避免对mylist进行两次独立迭代?这样做可以提高性能吗?


9
来到这里是为了寻找在集合构建器语句中使用条件的方法,你的问题回答了我的问题 :) - Anuvrat Parashar
7
“split”在这个操作中的描述是不幸的,因为它在Python字符串方面已经有了特定的含义。我认为“divide”是一个更精确(或者至少在Python可迭代对象的上下文中不太容易混淆)的词来描述这个操作。我在这里查找与str.split()相当的列表等效方法,以将列表分割成一组连续的子列表。例如,split([1,2,3,4,5,3,6], 3) -> ([1,2],[4,5],[6]),而不是按类别划分列表元素。 - Stew
在python-list上讨论了同一主题的内容。讨论链接 - Xiong Chiamiov
IMAGE_TYPES 应该是一个集合而不是元组:IMAGE_TYPES = set('.jpg','.jpeg','.gif','.bmp','.png')。在可读性上几乎没有区别,应该使用 n(1) 而不是 n(o/2)。 - ChaimG
40个回答

321

手动迭代,使用条件选择一个列表,将每个元素附加到该列表:

good, bad = [], []
for x in mylist:
    (bad, good)[x in goodvals].append(x)

20
真是太巧妙了!虽然我花了一些时间才理解发生了什么。我想知道其他人是否认为这可以被视为可读代码。 - jgpaiva
263
如果x在goodvals中,就执行good.append(x),否则执行bad.append(x)。这样的代码更易读。 - dansalmo
31
尤其是因为你可以使用for循环将其转换为一行代码,如果你想添加的内容比 x 更加复杂,你也可以将它变成一个 appendfor x in mylist: (好的 if isgood(x) else 不好的).append(x) - yo'
36
稍微简化一下:如果 x 在 goodvals 中就将其添加到good列表中,否则添加到bad列表中。 - Pi Delport
13
好的回答。虽然这段代码是否好还有争议,但我认为它展现了 Python 提供的灵活性,尽管我可能会说这是边界滥用。 - r.ook
显示剩余11条评论

152
good = [x for x in mylist if x in goodvals]
bad  = [x for x in mylist if x not in goodvals]

is there a more elegant way to do this?

那段代码非常易读,清晰明了!

# files looks like: [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ... ]
IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png')
images = [f for f in files if f[2].lower() in IMAGE_TYPES]
anims  = [f for f in files if f[2].lower() not in IMAGE_TYPES]

再次强调,这是没问题的!

使用集合可能会略微提高性能,但这只是微不足道的差距,而且我认为列表推导式更容易阅读,您也不必担心顺序被搞乱,重复项被删除等问题。

实际上,我可能会再往“后退”一步,只使用简单的for循环:

images, anims = [], []

for f in files:
    if f.lower() in IMAGE_TYPES:
        images.append(f)
    else:
        anims.append(f)

使用列表推导或使用set()是可以的,直到您需要添加其他检查或另一个逻辑 - 比如说您想要删除所有0字节的jpeg文件,您只需添加类似以下的内容..

if f[1] == 0:
    continue

61
有没有一种列表推导的方法,而不必两次循环遍历列表? - balki
45
问题在于这违反了DRY原则。如果有更好的方法来解决这个问题就太好了。 - Antimony
28
一旦对函数式编程(Haskell)或函数式风格(LINQ)产生了兴趣,我们开始觉得 Python 过时了—— [x for x in blah if ...] 写法啰嗦,lambda 笨拙且有限制……这就像今天开着1995年最酷的汽车,但感觉和当年不同了。 - Tomasz Gandor
10
值得一提的是,Haskell比Python更早(实际上也影响了Python的设计)。我认为列表推导式和lambda函数的语法故意保持得有点冗长,可能是为了避免过度使用它们。这确实是有一定风险的...尽管我喜欢Haskell,但我可以理解为什么许多人通常认为Python更易读。 - leftaroundabout
8
简单的for循环是完成这个任务最好的方式......一个循环,非常清晰易读。 - Anentropic
显示剩余8条评论

124

这里是惰性迭代器的方法:

from itertools import tee

def split_on_condition(seq, condition):
    l1, l2 = tee((condition(item), item) for item in seq)
    return (i for p, i in l1 if p), (i for p, i in l2 if not p)

它对每个项目仅评估一次条件,并返回两个生成器,第一个生成器产生条件为真的序列中的值,另一个生成器则产生条件为假的值。

由于它是惰性的,您可以在任何迭代器上使用它,甚至是无限的:

from itertools import count, islice

def is_prime(n):
    return n > 1 and all(n % i for i in xrange(2, n))

primes, not_primes = split_on_condition(count(), is_prime)
print("First 10 primes", list(islice(primes, 10)))
print("First 10 non-primes", list(islice(not_primes, 10)))

通常情况下,非惰性列表返回方法更好:
def split_on_condition(seq, condition):
    a, b = [], []
    for item in seq:
        (a if condition(item) else b).append(item)
    return a, b

编辑:对于你更具体的用例,需要按某个键将项目拆分为不同的列表,这里有一个通用的函数可以实现:

DROP_VALUE = lambda _:_
def split_by_key(seq, resultmapping, keyfunc, default=DROP_VALUE):
    """Split a sequence into lists based on a key function.

        seq - input sequence
        resultmapping - a dictionary that maps from target lists to keys that go to that list
        keyfunc - function to calculate the key of an input value
        default - the target where items that don't have a corresponding key go, by default they are dropped
    """
    result_lists = dict((key, []) for key in resultmapping)
    appenders = dict((key, result_lists[target].append) for target, keys in resultmapping.items() for key in keys)

    if default is not DROP_VALUE:
        result_lists.setdefault(default, [])
        default_action = result_lists[default].append
    else:
        default_action = DROP_VALUE

    for item in seq:
        appenders.get(keyfunc(item), default_action)(item)

    return result_lists

使用方法:

def file_extension(f):
    return f[2].lower()

split_files = split_by_key(files, {'images': IMAGE_TYPES}, keyfunc=file_extension, default='anims')
print split_files['images']
print split_files['anims']

你可能是对的,这违反了YAGNI原则。它基于这样一个假设:未来可以将事物分成不同列表的数量会增长。 - Ants Aasma
18
代码可能很多,但如果[ x for x in my_list if ExpensiveOperation(x) ]运行时间很长,你肯定不想重复执行它! - dash-tom-bang
1
+1 为提供多种解决方案,包括基于迭代器和特定的“in X”解决方案。虽然 OP 的“in goodvals”可能很小,但用一个非常大的字典或昂贵的谓词来替换它可能会很昂贵。此外,它减少了在每个需要使用列表推导两次的地方编写代码的需求,从而降低了引入拼写错误/用户错误的可能性。不错的解决方案。谢谢! - cod3monk3y
4
请注意,tee会存储其返回的迭代器之间的所有值,因此如果您循环遍历完一个生成器再遍历另一个生成器,它实际上并不会节省内存。 - John La Rooy

28

所有提出的解决方案都存在一个问题,那就是它将扫描并两次应用过滤函数。我会编写一个简单的小函数,如下所示:

def split_into_two_lists(lst, f):
    a = []
    b = []
    for elem in lst:
        if f(elem):
            a.append(elem)
        else:
            b.append(elem)
    return a, b

这样你就不会重复处理任何东西,也不会重复编写代码。


我同意。我正在寻找一种“优雅”的方法(即在此处意味着短小和内置/隐含),以便在不扫描列表两次的情况下完成此操作,但似乎(没有进行分析)这是正确的方法。当然,这只对大量数据有影响。 - Matthew Flaschen
在我看来,如果你知道一种使用更少的CPU(从而更少的耗电)的方法来完成它,那么没有理由不使用它。 - winden
5
将我所有的 Python 代码转化成 C。 ;) - Elliot Cameron

20

我的想法是提出一个懒惰、单遍、partition函数,可以保留输出子序列的相对顺序。

1. 要求

我假设这些要求是:

  • 保持元素的相对顺序(因此不使用集合和字典)
  • 每个元素只评估一次条件(因此不使用(i)filtergroupby
  • 允许懒惰地消耗任一序列(如果我们能够预先计算它们,那么朴素实现也可能是可接受的)

2. split

我的partition函数(下面介绍)和其他类似的函数已经成为一个小库:

它可以通过PyPI正常安装:

pip install --user split

要根据条件拆分列表,请使用 partition 函数:

>>> from split import partition
>>> files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi') ]
>>> image_types = ('.jpg','.jpeg','.gif','.bmp','.png')
>>> images, other = partition(lambda f: f[-1] in image_types, files)
>>> list(images)
[('file1.jpg', 33L, '.jpg')]
>>> list(other)
[('file2.avi', 999L, '.avi')]

3. partition 函数解释

在内部,我们需要同时构建两个子序列,所以仅消耗一个输出序列将强制计算另一个序列。而且我们需要在用户请求之间保留状态(存储已处理但尚未请求的元素)。为了保持状态,我使用了两个双端队列 (deques):

from collections import deque

SplitSeq 类负责基础的管理:

class SplitSeq:
    def __init__(self, condition, sequence):
        self.cond = condition
        self.goods = deque([])
        self.bads = deque([])
        self.seq = iter(sequence)

在它的.getNext()方法中发生了神奇的事情。它几乎像迭代器的.next()方法一样,但允许指定这次我们想要哪种类型的元素。在幕后,它不会丢弃被拒绝的元素,而是将它们放入两个队列中的一个:

    def getNext(self, getGood=True):
        if getGood:
            these, those, cond = self.goods, self.bads, self.cond
        else:
            these, those, cond = self.bads, self.goods, lambda x: not self.cond(x)
        if these:
            return these.popleft()
        else:
            while 1: # exit on StopIteration
                n = self.seq.next()
                if cond(n):
                    return n
                else:
                    those.append(n)
最终用户应该使用partition函数。它接受一个条件函数和一个序列(与mapfilter类似),并返回两个生成器。第一个生成器构建满足条件的元素的子序列,第二个生成器构建补集子序列。迭代器和生成器允许即使是长或无限序列也可以进行惰性划分。
def partition(condition, sequence):
    cond = condition if condition else bool  # evaluate as bool if condition == None
    ss = SplitSeq(cond, sequence)
    def goods():
        while 1:
            yield ss.getNext(getGood=True)
    def bads():
        while 1:
            yield ss.getNext(getGood=False)
    return goods(), bads()

我选择将测试函数作为第一个参数,以便将来可以进行部分应用(类似于mapfilter将测试函数作为第一个参数的方式)。


我仍然可以在PyPI上找到split包,但是Bitbucket存储库(也是PyPI上的项目主页链接)似乎已经消失了。 - Karl Knechtel
1
这难道不只是 more_itertools.bucket 的简化版本吗? - pypae

18

我基本上喜欢Anders的方法,因为它非常通用。这里有一个版本,将分类器放在第一位(以匹配筛选语法),并使用了一个默认字典(假设已导入)。

def categorize(func, seq):
    """Return mapping from categories to lists
    of categorized items.
    """
    d = defaultdict(list)
    for item in seq:
        d[func(item)].append(item)
    return d

我本来想从“Python之禅”中挑选出适用于此处的语句,但是这太多了,一个评论容纳不下。 =)非常棒的一段代码。 - jpmc26

14

优雅且快速

受DanSalmo评论的启发,这里提供一种简洁、优雅且同时也是最快解决方案之一。

good_set = set(goodvals)
good, bad = [], []
for item in my_list:
    good.append(item) if item in good_set else bad.append(item)

提示:将goodvals转换为一个集合会使我们获得更快的速度提升。

最快的

为了获得最大的速度,我们采用最快的答案,并通过将good_list转换为一个集合来进行加速。这单一的操作就能够给我们带来40%以上的速度提升,而且即使它仍然易读,我们最终得到的解决方案也比最慢的解决方案快5.5倍以上。

good_list_set = set(good_list)  # 40%+ faster than a tuple.

good, bad = [], []
for item in my_origin_list:
    if item in good_list_set:
        good.append(item)
    else:
        bad.append(item)

稍微简短一些

这是之前答案的更为简明版本。

good_list_set = set(good_list)  # 40%+ faster than a tuple.

good, bad = [], []
for item in my_origin_list:
    out = good if item in good_list_set else bad
    out.append(item)

优美之美有些主观,但一些像鲁伯·戈尔德堡式的可爱和独具匠心的解决方案相当令人担忧,并且不应该在任何语言的生产代码中使用,更不用说 Python 了,因为 Python 本质上是优雅的。


基准测试结果:

filter_BJHomer                  80/s       --   -3265%   -5312%   -5900%   -6262%   -7273%   -7363%   -8051%   -8162%   -8244%
zip_Funky                       118/s    4848%       --   -3040%   -3913%   -4450%   -5951%   -6085%   -7106%   -7271%   -7393%
two_lst_tuple_JohnLaRoy         170/s   11332%    4367%       --   -1254%   -2026%   -4182%   -4375%   -5842%   -6079%   -6254%
if_else_DBR                     195/s   14392%    6428%    1434%       --    -882%   -3348%   -3568%   -5246%   -5516%   -5717%
two_lst_compr_Parand            213/s   16750%    8016%    2540%     967%       --   -2705%   -2946%   -4786%   -5083%   -5303%
if_else_1_line_DanSalmo         292/s   26668%   14696%    7189%    5033%    3707%       --    -331%   -2853%   -3260%   -3562%
tuple_if_else                   302/s   27923%   15542%    7778%    5548%    4177%     343%       --   -2609%   -3029%   -3341%
set_1_line                      409/s   41308%   24556%   14053%   11035%    9181%    3993%    3529%       --    -569%    -991%
set_shorter                     434/s   44401%   26640%   15503%   12303%   10337%    4836%    4345%     603%       --    -448%
set_if_else                     454/s   46952%   28358%   16699%   13349%   11290%    5532%    5018%    1100%     469%       --

Python 3.7的完整基准测试代码(修改自FunkySayu):

good_list = ['.jpg','.jpeg','.gif','.bmp','.png']

import random
import string
my_origin_list = []
for i in range(10000):
    fname = ''.join(random.choice(string.ascii_lowercase) for i in range(random.randrange(10)))
    if random.getrandbits(1):
        fext = random.choice(list(good_list))
    else:
        fext = "." + ''.join(random.choice(string.ascii_lowercase) for i in range(3))

    my_origin_list.append((fname + fext, random.randrange(1000), fext))

# Parand
def two_lst_compr_Parand(*_):
    return [e for e in my_origin_list if e[2] in good_list], [e for e in my_origin_list if not e[2] in good_list]

# dbr
def if_else_DBR(*_):
    a, b = list(), list()
    for e in my_origin_list:
        if e[2] in good_list:
            a.append(e)
        else:
            b.append(e)
    return a, b

# John La Rooy
def two_lst_tuple_JohnLaRoy(*_):
    a, b = list(), list()
    for e in my_origin_list:
        (b, a)[e[2] in good_list].append(e)
    return a, b

# # Ants Aasma
# def f4():
#     l1, l2 = tee((e[2] in good_list, e) for e in my_origin_list)
#     return [i for p, i in l1 if p], [i for p, i in l2 if not p]

# My personal way to do
def zip_Funky(*_):
    a, b = zip(*[(e, None) if e[2] in good_list else (None, e) for e in my_origin_list])
    return list(filter(None, a)), list(filter(None, b))

# BJ Homer
def filter_BJHomer(*_):
    return list(filter(lambda e: e[2] in good_list, my_origin_list)), list(filter(lambda e: not e[2] in good_list,                                                                             my_origin_list))

# ChaimG's answer; as a list.
def if_else_1_line_DanSalmo(*_):
    good, bad = [], []
    for e in my_origin_list:
        _ = good.append(e) if e[2] in good_list else bad.append(e)
    return good, bad

# ChaimG's answer; as a set.
def set_1_line(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        _ = good.append(e) if e[2] in good_list_set else bad.append(e)
    return good, bad

# ChaimG set and if else list.
def set_shorter(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        out = good if e[2] in good_list_set else bad
        out.append(e)
    return good, bad

# ChaimG's best answer; if else as a set.
def set_if_else(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        if e[2] in good_list_set:
            good.append(e)
        else:
            bad.append(e)
    return good, bad

# ChaimG's best answer; if else as a set.
def tuple_if_else(*_):
    good_list_tuple = tuple(good_list)
    good, bad = [], []
    for e in my_origin_list:
        if e[2] in good_list_tuple:
            good.append(e)
        else:
            bad.append(e)
    return good, bad

def cmpthese(n=0, functions=None):
    results = {}
    for func_name in functions:
        args = ['%s(range(256))' % func_name, 'from __main__ import %s' % func_name]
        t = Timer(*args)
        results[func_name] = 1 / (t.timeit(number=n) / n) # passes/sec

    functions_sorted = sorted(functions, key=results.__getitem__)
    for f in functions_sorted:
        diff = []
        for func in functions_sorted:
            if func == f:
                diff.append("--")
            else:
                diff.append(f"{results[f]/results[func]*100 - 100:5.0%}")
        diffs = " ".join(f'{x:>8s}' for x in diff)

        print(f"{f:27s} \t{results[f]:,.0f}/s {diffs}")


if __name__=='__main__':
    from timeit import Timer
cmpthese(1000, 'two_lst_compr_Parand if_else_DBR two_lst_tuple_JohnLaRoy zip_Funky filter_BJHomer if_else_1_line_DanSalmo set_1_line set_if_else tuple_if_else set_shorter'.split(" "))

我鼓励你也测试一下我的答案 https://dev59.com/WnNA5IYBdhLWcg3wdtv5#71985816,经过我的测试,它的结果非常有竞争力,同时也很易读。 - JustinFisher
在对算法的相对性能进行评估时,最好研究输入数据如何影响相对性能。当输入数据更长或更短、包含更多或更少的重复项、被测试条件更昂贵或更便宜时,某些算法是否相对更好?即使每个算法具有相同的大O复杂度,这些因素也可能产生很大的差异。 - Karl Knechtel

14

首次尝试(在 OP 编辑之前):使用集合:

mylist = [1,2,3,4,5,6,7]
goodvals = [1,3,7,8,9]

myset = set(mylist)
goodset = set(goodvals)

print list(myset.intersection(goodset))  # [1, 3, 7]
print list(myset.difference(goodset))    # [2, 4, 5, 6]

这对于可读性(我的看法)和性能都是有益的。

第二步(在原帖编辑后发布):

将你的好扩展列表创建为一个集合:

IMAGE_TYPES = set(['.jpg','.jpeg','.gif','.bmp','.png'])

否则,这看起来对我来说很好,但这样做可以提高性能。


4
如果在分割之前列表已经按照一定顺序排列,并且您希望它们保持这种顺序,那么这不是最佳解决方案。 - Daniyar
8
那样不就去掉了重复的部分吗? - mavnn
创建一个集合的时间复杂度为O(n log n)。两次迭代列表的时间复杂度为O(n)。尽管集合解决方案可能更加优雅(当它首先是正确的),但随着n的增加,它肯定会变得更慢。 - dash-tom-bang
1
@dash-tom-bang 遍历列表的时间复杂度为O(n * n)。这是因为列表中的每个项都可能需要与goodvals中的每个项进行比较。 - ChaimG
@ChaimG 说得好,不过我们还需要考虑交集和差集操作的成本(我不知道具体数字,但我相当确定它们也是超线性的)。 - dash-tom-bang
啊-好眼力;创建一个集合,然后通过检查集合成员来迭代另一个列表! :) - dash-tom-bang

13

itertools.groupby 几乎可以做到您想要的,但是它需要对项目进行排序以确保您获得单个连续范围,因此您需要先按键排序(否则您将为每个类型获取多个交错的组)。 例如:

def is_good(f):
    return f[2].lower() in IMAGE_TYPES

files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ('file3.gif', 123L, '.gif')]

for key, group in itertools.groupby(sorted(files, key=is_good), key=is_good):
    print key, list(group)

提供:

False [('file2.avi', 999L, '.avi')]
True [('file1.jpg', 33L, '.jpg'), ('file3.gif', 123L, '.gif')]

与其他解决方案类似,可以定义key func以将其分成任意数量的组。


11
good.append(x) if x in goodvals else bad.append(x)

这个优雅而简明的回答由@dansalmo在评论中被埋没了,所以我在这里重新发布作为一个回答,以便它能够获得应有的重视,特别是对于新读者。

完整的示例:

good, bad = [], []
for x in my_list:
    good.append(x) if x in goodvals else bad.append(x)

或者 (bad, good)[x in goodvals].append(x) - malthe

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