Python 2.6 itertools docs 中有一个很好的示例,它展示了如何查找连续序列。引用:
使用groupby 查找连续数字的运行。的关键
解决方案与一个范围不同,因此连续的数字都是
出现在同一组中。
由于某些奇怪的原因,该示例不在文档的更高版本中。该代码适用于数字序列,下面的代码显示了如何使其适用于字母。
from itertools import groupby
s = 'jaghiuuabc'
def keyfunc(t):
''' Subtract the character's index in the string
from its Unicode codepoint number.
'''
i, c = t
return ord(c) - i
a = []
for k, g in groupby(enumerate(s), key=keyfunc):
# Extract the chars from the (index, char) tuples in the group
seq = [t[1] for t in g]
if len(seq) > 1:
a.append(''.join(seq))
print(a)
输出
['ghi', 'abc']
工作原理
这段代码的核心是
groupby(enumerate(s), key=keyfunc)
enumerate(s) 为s 中的每个字符生成包含索引号和字符的元组。例如:
s = 'ABCEF'
for t in enumerate(s):
print(t)
输出
(0, 'A')
(1, 'B')
(2, 'C')
(3, 'E')
(4, 'F')
groupby 从序列或迭代器中获取项目,并将相邻的相等项目聚集到组中。默认情况下,它只是比较项目的值以查看它们是否相等。但你也可以给它一个关键的功能。当您这样做时,它会将每个项目传递给 key 函数,并使用该 key 函数返回的结果进行相等性测试。
这是一个简单的例子。首先,我们定义了一个函数div_by_10,它使用整数除法将一个数字除以10。这基本上摆脱了数字中的最后一位。
def div_by_10(n):
return n // 10
a = [2, 5, 10, 13, 17, 21, 22, 29, 33, 35]
b = [div_by_10(u) for u in a]
print(a)
print(b)
输出
[2, 5, 10, 13, 17, 21, 22, 29, 33, 35]
[0, 0, 1, 1, 1, 2, 2, 2, 3, 3]
因此,如果我们使用div_by_10 作为groupby 的键函数,它将忽略每个数字中的最后一个数字,因此如果它们仅在最后一个数字上不同,它将把相邻的数字组合在一起。
from itertools import groupby
def div_by_10(n):
return n // 10
a = [2, 5, 10, 13, 17, 21, 22, 29, 33, 35]
print(a)
for key, group in groupby(a, key=div_by_10):
print(key, list(group))
输出
[2, 5, 10, 13, 17, 21, 22, 29, 33, 35]
0 [2, 5]
1 [10, 13, 17]
2 [21, 22, 29]
3 [33, 35]
我的keyfunc 接收一个 (index_number, character) 元组并从字符的代码号中减去该 index_number 并返回结果。让我们看看我之前的 'ABCEF' 示例有什么作用:
def keyfunc(t):
i, c = t
return ord(c) - i
for t in enumerate('ABCEF'):
print(t, keyfunc(t))
输出
(0, 'A') 65
(1, 'B') 65
(2, 'C') 65
(3, 'E') 66
(4, 'F') 66
'A' 的代号是 65,'B' 的代号是 66,'C' 的代号是 67,等等。所以当我们从每个 'A' 的代号中减去索引时'、'B' 和 'C' 我们得到 65。但是我们跳过了 'D',所以当我们对 'E' 和 'F' 进行减法时,我们得到 66。这就是 groupby 可以输入 'A' 的方式、“B”和“C”在一组中,“E”和“F”在下一组中。
这可能是个棘手的问题。不要指望一下子就能完全理解。但如果你自己做一些实验,我相信它会逐渐深入。;)
只是为了好玩,这里是该代码的不可读的多重嵌套列表理解版本。 ;)
print([z for _, g in groupby(enumerate(s),lambda t:ord(t[1])-t[0])for z in[''.join([*zip(*g)][1])]if len(z)>1])
这是另一个受Amit Tripathi's answer 启发的版本。这个不使用任何导入,因为它手动进行分组。 prev 包含前一个字符的代码点编号。我们将prev 初始化为-2,以便第一次执行if i != prev + 1 测试时保证为真,因为ord(ch) 的最小可能值为零,因此将向groups 添加一个新的空列表。
s = 'jaghiuuabcxyzq'
prev, groups = -2, []
for ch in s:
i = ord(ch)
if i != prev + 1:
groups.append([])
groups[-1].append(ch)
prev = i
print(groups)
a = [''.join(u) for u in groups if len(u) > 1]
print(a)
输出
[['j'], ['a'], ['g', 'h', 'i'], ['u'], ['u'], ['a', 'b', 'c'], ['x', 'y', 'z'], ['q']]
['ghi', 'abc', 'xyz']