在匹配元素处划分列表的 Pythonic 和高效方法是什么?



这与Python非常相似:根据条件拆分列表?,并且还 https://nedbatchelder.com/blog/201306/filter_a_list_into_two_parts.html 但是,我想在第一个不符合谓词的元素处将列表分成两部分,而不是基于谓词将单个元素划分为两个列表。

>>> divide_list(lambda x: x < 7, list(range(10)))
([0, 1, 2, 3, 4, 5, 6], [7, 8, 9])
>>> divide_list(lambda x: x < 7, [1, 3, 5, 7, 9, 5])
([1, 3, 5], [7, 9, 5])
>>> divide_list(lambda x: x < 7, [7, 9, 5])
([], [7, 9, 5])
>>> divide_list(lambda x: x < 7, [1, 3, 5])
([1, 3, 5], [])
>>> divide_list(lambda x: x['a'], [{'a': True, 'b': 1}, {'a': True}, {'a': False}])
([{'a': True, 'b': 1}, {'a': True}], [{'a': False}])

注意事项:

  • 输入列表可能无法排序
  • 输入列表可能包含重复元素
  • 理想情况下,我们不想多次评估条件(对于每个元素,如果值重复,那没关系)
  • 理想情况下,它将接受迭代器作为输入(即只能对输入数据进行一次传递)
  • 返回迭代器是可以接受

我认为朴素的实现可能是最好的,除非你真的需要迭代器作为输出。如果您的输入流是一个迭代器,并且您没有足够的内存来一次实现整个事情,等等,这可能很有用。

在这种情况下,我认为itertools很棒。 我最初的直觉是做这样的事情:

# broken  :-(
def divide_iter(pred, lst):
i = iter(lst)
yield itertools.takewhile(lst, pred)
yield i

不幸的是,由于各种原因,这不起作用。 最值得注意的是,它删除了一个元素。 即使没有,如果您在转到下一个列表之前没有使用整个可迭代takewhile,也可能会遇到问题。 我认为第二个问题将是我们在使用迭代器时遇到的一个问题,所以这有点令人沮丧,但这是我们为逐个元素处理事物而不是一次实现整个列表而付出的代价。

相反,让我们考虑根据谓词是否返回 true 对项目进行分组。 然后groupby变得更有吸引力 - 唯一的事情是我们需要跟踪谓词是否返回了 True。 有状态函数并不有趣,因此,我们可以使用一个类并将绑定方法作为关键参数传递给groupby

import itertools
class _FoundTracker(object):
def __init__(self, predicate):
self.predicate = predicate
self._found = False
def check_found(self, value):
if self._found:
return True
else:
self._found = self.predicate(value)
return self._found
def split_iterable(iterable, predicate):
tracker = _FoundTracker(predicate)
for i, (k, group) in enumerate(itertools.groupby(iterable, key=tracker.check_found)):
yield group
if i == 0:
yield iter(())
if __name__ == '__main__':
for group in split_iterable(xrange(10), lambda x: x < 5):
print(list(group))

这也有一些可能时髦的行为......要进行演示,请考虑:

g1, g2 = split_iterable(xrange(10), lambda x: x > 5)
print(list(g1))
print(list(g2))

你会看到你得到了一些非常奇怪的行为:-)。 或者:

g1, g2 = map(list, split_iterable(range(10), lambda x: x > 5))
print(g1)
print(g2)

应该工作正常。

一个让事情顺利进行的天真的实现:

def divide_list(pred, lst):
before, after = [], []
found = False
for item in lst:
if not found:
if pred(item):
before.append(item)
else:
found = True
if found:
after.append(item)
return before, after

这基本上是你幼稚的尝试,但没有使用单独的布尔标志来确定谓词何时失败;它只是使用对第一个列表的引用,然后是另一个列表来执行追加。

def divide_list(pred, lst):
a, b = [], []
curr = a
for x in lst:
if curr is a and not pred(x):
curr = b
curr.append(x)
return a, b

这是我相对有效的尝试:

from collections import Hashable
def divide_list(pred, list):
# The predicate may be expensive, so we can
# store elements that have already been checked
# in a set for fast verification.
elements_checked = set()
# Assuming that every element of the list is of
# the same type and the list is nonempty, we can
# store a flag to check if an element is hashable.
hashable = isinstance(list[0], Hashable)
for index, element in enumerate(list):
if hashable and element in elements_checked:
continue
if not pred(element):
return list[:index], list[index:]
if hashable:
elements_checked.add(element)
return list, []

如果你要用其他答案来衡量这个问题,我认为这将是最快的。

顺便说一下,我喜欢这个问题!

如果可能简单,为什么复杂?已经提到过,但在我看来,无法理解的原因从考虑中消失了:itertoolstakewhile的使用。

下面的代码通过了所有断言测试,函数本身需要三行代码:

from itertools import takewhile
def divide_list(pred, lstL):
header  = list(takewhile(pred, lstL))
trailer = lstL[len(header):]
return header, trailer

assert divide_list(lambda x: x < 7, list(range(10))) == ([0, 1, 2, 3, 4, 5, 6], [7, 8, 9])
assert divide_list(lambda x: x < 7, [1, 3, 5, 7, 9, 5]) == ([1, 3, 5], [7, 9, 5])
assert divide_list(lambda x: x < 7, [7, 9, 5]) == ([], [7, 9, 5])
assert divide_list(lambda x: x < 7, [1, 3, 5]) == ([1, 3, 5], [])
assert divide_list(lambda x: x['a'], [{'a': True, 'b': 1}, {'a': True}, {'a': False}]) == ([{'a': True, 'b': 1}, {'a': True}], [{'a': False}])

最新更新