如何根据谓词拆分序列?

时间:2012-01-09 19:09:04

标签: python

我经常遇到需要将一个序列拆分为满足和不满足给定谓词的元素的两个子序列(保留原始的相对排序)。

这个假设的“分离器”功能在行动中看起来像这样:

>>> data = map(str, range(14))
>>> pred = lambda i: int(i) % 3 == 2
>>> splitter(data, pred)
[('2', '5', '8', '11'), ('0', '1', '3', '4', '6', '7', '9', '10', '12', '13')]

我的问题是:

  

Python是否已经采用标准/内置方式来实现这一目标?

这个功能当然不难编码(参见下面的附录),但由于种种原因,我宁愿使用标准/内置方法而不是自动方法。

谢谢!



附录:

到目前为止,我在Python中处理此任务的最佳标准函数是itertools.groupby。但是,要将它用于此特定任务,必须为每个列表成员调用谓词函数两次,我觉得这很烦人:

>>> import itertools as it
>>> [tuple(v[1]) for v in it.groupby(sorted(data, key=pred), key=pred)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

(上面的最后一个输出与前面所示的那个不同之处在于,满足谓词的元素的子序列是最后一个而不是第一个,但这非常小,如果需要的话很容易修复。)

可以避免对谓词的冗余调用(基本上是通过“内联memoization”),但我对此的最好的准备工作相当精细,与splitter(data, pred)的简单性相去甚远:

>>> first = lambda t: t[0]
>>> [zip(*i[1])[1] for i in it.groupby(sorted(((pred(x), x) for x in data),
... key=first), key=first)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

顺便说一句,如果您不关心保留原始排序,sorted的默认排序顺序可以完成工作(因此key参数可能会从sorted中省略呼叫):

>>> [zip(*i[1])[1] for i in it.groupby(sorted(((pred(x), x) for x in data)),
... key=first)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

6 个答案:

答案 0 :(得分:31)

我知道你说你不想写自己的功能,但我无法想象为什么。您的解决方案涉及编写自己的代码,您只是没有将它们模块化为函数。

这完全符合您的要求,可以理解,并且每个元素只评估一次谓词:

def splitter(data, pred):
    yes, no = [], []
    for d in data:
        if pred(d):
            yes.append(d)
        else:
            no.append(d)
    return [yes, no]

如果您希望它更紧凑(出于某种原因):

def splitter(data, pred):
    yes, no = [], []
    for d in data:
        (yes if pred(d) else no).append(d)
    return [yes, no]

答案 1 :(得分:15)

分区是其中一个itertools recipes就是这样做的。它使用tee()来确保它在一次传递中迭代集合,尽管有多个迭代器,内置filter()函数来获取满足谓词的项目以及filterfalse()以获得相反的效果过滤器。这就像你要获得标准/内置方法一样接近。

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)

答案 2 :(得分:2)

如果你不关心效率,我认为groupby(或任何"将数据放入n箱子"功能)有一些很好的对应关系,

by_bins_iter = itertools.groupby(sorted(data, key=pred), key=pred)
by_bins = dict((k, tuple(v)) for k, v in by_bins_iter)

然后,您可以通过

找到解决方案
return by_bins.get(True, ()), by_bins.get(False, ())

答案 3 :(得分:1)

内置模块more_itertools中有一个名为partition的函数,它完全可以满足topicstarter的要求。

from more_itertools import partition

numbers = [1, 2, 3, 4, 5, 6, 7]
predicate_false, predicate_true = partition(lambda x: x % 2 == 0, numbers)

print(list(predicate_false), list(predicate_true))

结果为[1, 3, 5, 7] [2, 4, 6]

答案 4 :(得分:0)

使用groupby对OP的一个实现和上面另一个评论者的实现略有不同:

groups = defaultdict(list, { k : list(ks) for k, ks in groupby(items, f) })

groups[True] == the matching items, or [] if none returned True
groups[False] == the non-matching items, or [] if none returned False

可悲的是,正如您所指出的那样,groupby要求首先按谓词对项目进行排序,因此如果不能保证,则需要这样:

groups = defaultdict(list, { k : list(ks) for k, ks in groupby(sorted(items, key=f), f) })

非常满口,但它是一个单独的表达式,它仅使用内置函数通过谓词对列表进行分区。

我认为您可以在没有sorted参数的情况下使用key,因为groupby在从关键功能点击新值时会创建一个新组。所以sorted只有在项目由提供的谓词自然排序时才有效。

答案 5 :(得分:0)

作为一种稍微通用的分区解决方案,请考虑分组。考虑以下函数,此函数是受clojure的group-by函数启发的。

您给它提供了一组要分组的项目以及一个将其分组的功能。这是代码:

def group_by(seq, f):

    groupings = {}

    for item in seq:
        res = f(item)
        if res in groupings:
            groupings[res].append(item)
        else:
            groupings[res] = [item]

    return groupings

对于OP的原始情况:

y = group_by(range(14), lambda i: int(i) % 3 == 2)
{False: [0, 1, 3, 4, 6, 7, 9, 10, 12, 13], True: [2, 5, 8, 11]}

按字符串长度对序列中的元素进行分组的更一般的情况:

x = group_by(["x","xx","yy","zzz","z","7654321"], len)
{1: ['x', 'z'], 2: ['xx', 'yy'], 3: ['zzz'], 7: ['7654321']}

这可以扩展到很多情况,并且是功能语言的核心功能。它与动态类型的python配合使用非常好,因为结果映射中的键可以是任何类型。享受吧!