哨兵连续出现两次时停止迭代的简洁方法

Concise way of stopping iteration when sentinel occurs twice in a row

我正在寻找一种生成迭代器的方法,该迭代器采用可迭代对象并仅传递值,直到标记值直接连续出现两次。与 iter(a.__next__, sentinel) 类似,只是标记必须出现两次。

以下相当平淡的代码可以解决问题,但肯定有一个不那么冗长的解决方案?

所以把它放在一个具体的问题中:

有没有办法避免使用成熟的生成器并使用 itertools 或生成器表达式来实现同样的效果?

>>> def repeat_offenders(a, sentinel):
...    ia = iter(a)
...    for x in ia:
...       if x==sentinel:
...          try:
...             y = next(ia)
...          except StopIteration:
...             yield x
...             raise
...          if y==sentinel:
...             raise StopIteration
...          yield x
...          yield y
...       else:
...          yield x

这里有两个例子:

>>> ''.join(repeat_offenders('ABCABCAABBCC', 'B'))
'ABCABCAA'
>>> ''.join(repeat_offenders('ABABAB', 'B'))
'ABABAB'

注意 this question 类似但缺少发电机角度。

您可以根据 iwindow、滑动 window 配方(它可以在任何可迭代对象上工作,而不仅仅是序列)和通常的 iter(callable, sentinel) 成语:

import itertools as IT

def iwindow(iterable, n=2):
    """
    Returns a sliding window (of width n) over data from the iterable.
    s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ..., (sk, None, ..., None)
    """
    iterables = IT.tee(iterable, n)
    iterables = (IT.islice(it, pos, None) for pos, it in enumerate(iterables))
    yield from IT.zip_longest(*iterables)

def repeat_offenders(iterable, sentinel, repeat=2):
    return (item[0] for item in iter(iwindow(iterable, repeat).__next__, 
                                     (sentinel,)*repeat))

print(''.join(repeat_offenders('ABCABCAABBCC', 'B', 2)))
# ABCABCAA

print(''.join(repeat_offenders('ABABAB', 'B', 2)))
# ABABAB

iwindow 是 itertools 文档中显示的 pairwise recipe 的概括。通过将 repeat_offenders 写成 iwindow,我们可以将概念概括为在 n 几乎免费重复后停止。

不确定是否有帮助,但我相信您的代码可以写得更简洁,如下所示:

from itertools import zip_longest

def repeat_offenders_jp(a, s):
    for i, j in zip_longest(a, a[1:]):
        if i == j and i == s:
            break
        else:
            yield i

''.join(repeat_offenders_jp('ABCABCAABBCC', 'B'))  # 'ABCABCAA'
''.join(repeat_offenders_jp('ABABAB', 'B'))        # 'ABABAB'

此处 zip_longest 为@jp_data_analysis 建议但为 "one-liner" 和 takewhile:

from itertools import zip_longest, takewhile

sentinel = 'B'
string = 'ABCABCAABBCC'
"".join(t[0] for t
        in takewhile(lambda t: t[0] != sentinel or t[0] != t[1], 
        zip_longest(string, string[1:])))
# 'ABCABCAA'

string = 'ABABAB'
"".join(t[0] for t
        in takewhile(lambda t: t[0] != sentinel or t[0] != t[1], 
        zip_longest(string, string[1:])))
# 'ABABAB'