Python 匹配列表中的一个元素,然后在它前面返回'n'个元素,在它后面返回'm'个元素

Python 匹配列表中的一个元素,然后在它前面返回'n'个元素,在它后面返回'm'个元素,python,iterator,itertools,Python,Iterator,Itertools,我的代码中的一个常见模式是:“搜索列表,直到找到一个特定的元素,然后查看它之前和之后的元素。” 例如,我可能想查看一个日志文件,其中重要事件用星号标记,然后拉出重要事件的上下文 在以下示例中,我想知道超光速驱动器爆炸的原因: Spinning up the hyperdrive Hyperdrive speed 100 rpm Hyperdrive speed 200 rpm Hyperdrive lubricant levels low (100 gal.) * CRITICA

我的代码中的一个常见模式是:“搜索列表,直到找到一个特定的元素,然后查看它之前和之后的元素。”

例如,我可能想查看一个日志文件,其中重要事件用星号标记,然后拉出重要事件的上下文

在以下示例中,我想知道超光速驱动器爆炸的原因:

  Spinning up the hyperdrive
  Hyperdrive speed 100 rpm
  Hyperdrive speed 200 rpm
  Hyperdrive lubricant levels low (100 gal.)
* CRITICAL EXISTENCE FAILURE
  Hyperdrive exploded
我需要一个函数,
get\u item\u with_context()
,该函数允许我找到带星号的第一行,然后给出它前面的
n
行和后面的
m

我的尝试如下:

import collections, itertools
def get_item_with_context(predicate, iterable, items_before = 0, items_after = 0):
    # Searches through the list of `items` until an item matching `predicate` is found.
    # Then return that item.
    # If no item matching predicate is found, return None.
    # Optionally, also return up to `items_before` items preceding the target, and
    # `items after` items after the target.
    #
    # Note:
    d = collections.deque (maxlen = items_before + 1 + items_after)
    iter1 = iterable.__iter__()
    iter2 = itertools.takewhile(lambda x: not(predicate(x)), iter1)    
    d.extend(iter2)

    # zero-length input, or no matching item
    if len(d) == 0 or not(predicate(d[-1])):
        return None

    # get context after match:
    try:
        for i in xrange(items_after):
            d.append(iter1.next())
    except StopIteration:
        pass

    if ( items_before == 0 and items_after == 0):
        return d[0]
    else:
        return list(d)
用法应如下所示:

>>> get_item_with_context(lambda x: x == 3, [1,2,3,4,5,6],
                          items_before = 1, items_after = 1)
[2, 3, 4]
这方面的问题:

  • 由于某些原因,使用
    not(谓词(d[-1])
    检查以确保我们确实找到了匹配项是无效的。它总是返回false
  • 如果在找到匹配项后,列表中的项之后少于
    项,则结果为垃圾
  • 其他边缘案例

我能就如何使这项工作更有效/更稳健提出一些建议吗?或者,如果我要重新发明轮子,也可以随意告诉我。

这可能是一个完全“非音速”的解决方案:

import itertools

def get_item_with_context(predicate, iterable, items_before = 0, items_after = 0):
    found_index = -1
    found_element = None

    before = [None] * items_before # Circular buffer

    after = []
    after_index = 0

    for element, index in zip(iterable, itertools.count()):
        if found_index >= 0:
            after += [element]
            if len(after) >= items_after:
                break
        elif predicate(element):
            found_index = index
            found_element = element
            if not items_after:
                break
        else:
            if items_before > 0:
                before[after_index] = element
                after_index = (after_index + 1) % items_before

    if found_index >= 0:
        if after_index:
            # rotate the circular before-buffer into place
            before = before[after_index:] + before[0:after_index]
        if found_index - items_before < 0:
            # slice off elements that "fell off" the start
            before = before[items_before - found_index:]
        return before, found_element, after

    return None

for index in range(0, 8):
    x = get_item_with_context(lambda x: x == index, [1,2,3,4,5,6], items_before = 1, items_after = 2)
    print(index, x)
我随意更改了输出,以使其更清楚地匹配谓词的内容以及前后的内容:

([2], 3, [4, 5])
  ^   ^    ^
  |   |    +-- after the element
  |   +------- the element that matched the predicate
  +----------- before the element
该函数处理:

  • 未找到项,返回
    None
    (如果要返回其他内容,则返回函数的最后一行)
  • 在元素未完全完成之前(即,找到的元素离开始太近,无法真正获取之前的
    N
    元素)
  • 元素未完全完成后(太接近结尾时相同)
  • 项目前或项目后设置为0(该方向没有上下文)
它使用:

  • 一个用于before元素的简单循环缓冲区,将其旋转到位以获得正确顺序的元素
  • before元素的简单列表
  • 任何iterable都不需要可索引集合,不会多次枚举任何元素,并且在找到所需的上下文后将停止

您可以使用对象获取上下文的环形缓冲区。要获得+/-2行上下文,请按如下方式初始化它:

context = collections.deque(maxlen=5)
然后迭代任何您喜欢的内容,对每一行调用:

context.append(line)

上下文[2]
上进行匹配,并为每个匹配输出整个deque内容。

我不确定我是否遗漏了问题中的某些内容,但这可以简单地按如下方式完成

>>> def get_item_with_context(predicate, iterable, items_before = 0, items_after = 0):
    queue = collections.deque(maxlen=items_before+1)
    found = False
    for e in iterable:
        queue.append(e)
        if not found and predicate(e):
            queue = collections.deque(queue,items_before+1+items_after)
            found = True
        if found:
            if not items_after : break
            items_after-=1
    if not found:
        queue.clear()
    return list(queue)

>>> get_item_with_context(lambda x: x == 0, [1,2,3,4,5,6],items_before = 2, items_after = 1)
[]
>>> get_item_with_context(lambda x: x == 4, [1,2,3,4,5,6],items_before = 2, items_after = 1)
[2, 3, 4, 5]
>>> get_item_with_context(lambda x: x == 1, [1,2,3,4,5,6],items_before = 2, items_after = 1)
[1, 2]
>>> get_item_with_context(lambda x: x == 6, [1,2,3,4,5,6],items_before = 2, items_after = 1)
[4, 5, 6]
>>> get_item_with_context(lambda x: x == 4, [1,2,3,4,5,6],items_before = 20, items_after = 10)
[1, 2, 3, 4, 5, 6]
或者:

def contextGet(iterable, predicate, before, after):
    it1, it2 = tee(it)
    log = deque(maxlen = (before + after + 1))
    for i in chain(dropwhile(lambda x: not predicate(x), it1), xrange(after + 1)):
        try:
            log.append(it2.next())
        except StopIteration:
            break
    return log

如果列表的其余部分比
after
参数短,则第二个参数可能返回过多的“before”元素。

下面是一些较短的内容:

import collections

def context_match(predicate, iterable, before = 0, after = 0):
    pre = collections.deque(maxlen = before + 1)
    post = []
    match = 0
    for el in iterable:
        if not match:
            pre.append(el)
            if predicate(el):
                match = 1
        elif match:
            if len(post) == after:
                break
            post.append(el)
    if not match:
        return
    output = list(pre)
    output.extend(post)
    return output

for val in xrange(8):
    print context_match(lambda x: x == val, [1,2,3,4,5,6],before = 2, after = 2)
#Output:
None
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]
[2, 3, 4, 5, 6]
[3, 4, 5, 6]
[4, 5, 6]
None
import collections
from itertools import islice

def windowfilter(pred, it, before=0, after=0):
        size = before + 1 + after
        q = collections.deque(maxlen=size)
        it = iter(it)
        for x in it:
                q.append(x)
                if pred(x):
                        # ok we got the item, add the trailing lines
                        more = list(islice(it, after))
                        q.extend(more)

                        # maybe there were too few items left
                        got = before + 1 + len(more)

                        # slice from the end
                        return tuple(q)[-got:]
测试结果:

seq = [1,2,3,4,5,6]
for elem in range(8):
        print elem, windowfilter((lambda x:x==elem), seq, 2, 1)

# Output:
0 None
1 (1, 2)
2 (1, 2, 3)
3 (1, 2, 3, 4)
4 (2, 3, 4, 5)
5 (3, 4, 5, 6)
6 (4, 5, 6)
7 None

这似乎可以正确处理边缘情况:

from collections import deque

def item_with_context(predicate, seq, before=0, after=0):
    q = deque(maxlen=before)
    it = iter(seq)

    for s in it:
        if predicate(s):
            return list(q) + [s] + [x for _,x in zip(range(after), it)]
        q.append(s)

我的答案是

for k,v in enumerate(iterable):
    #if cmp(v,predicate) == 0:
    if v == predicate:
        if k+items_after < len(iterable):
            res.append((' '.join(token[(k-items_before):(k+items_after+1)]))) 
        elif k+window == len(token):
            res.append((' '.join(token[(k-items_before):])))   
        else:
            res.append((' '.join(token[(k-items_before):])))

return res
枚举中k,v的
(iterable):
#如果cmp(v,谓词)==0:
如果v==谓词:
如果k+项在
这是你用切片无法完成的吗?@BurhanKhalid:我可能用的是无法倒转的iterables。你为什么用
iterable.\uu iter\uu()
而不是
iter(iterable)
?@jamylak:因为我很傻。)我不明白你的结果。它不应该是
[2,3,4]
?此外,我还想知道,如果可以通过grep等工具轻松完成一些事情,那么选择Python有什么理由(除了你讽刺的回答我很傻:-)你可以说服Python deque充当循环缓冲区(通过使用max_length参数初始化)。这可能比滚动你自己的循环缓冲区要好一点这实际上就是我的示例代码试图做的,假设您要查找的元素确实存在,并且前后有适当数量的元素,那么它工作得很好。但是有很多边缘案例。我们有一个赢家!简洁,通过了我所有的测试用例。谢谢。:)(使用
zip()
的“在最短序列上终止”行为来限制元素的数量-这是一个很好的方法。)有一个微妙的错误-从
it
中消耗了太多的项目,因为
zip(it,range(after))
在检查
范围中是否有任何内容之前先获取
it
(after)
。如果你交换参数的顺序,效果很好。我已经更正了你的代码。@Li aungYip:你能提供一个显示错误的测试用例吗?我不确定我是否理解。当然。尝试类似于
a=xrange(10);item_与_上下文(lambda x:x==3,a,0,0);a.next()
。您希望函数使用迭代器直到
3
,然后停止,因此
a.next()
4
。但使用原始代码,您实际上会得到
5
from collections import deque

def item_with_context(predicate, seq, before=0, after=0):
    q = deque(maxlen=before)
    it = iter(seq)

    for s in it:
        if predicate(s):
            return list(q) + [s] + [x for _,x in zip(range(after), it)]
        q.append(s)
for k,v in enumerate(iterable):
    #if cmp(v,predicate) == 0:
    if v == predicate:
        if k+items_after < len(iterable):
            res.append((' '.join(token[(k-items_before):(k+items_after+1)]))) 
        elif k+window == len(token):
            res.append((' '.join(token[(k-items_before):])))   
        else:
            res.append((' '.join(token[(k-items_before):])))

return res