一个常见的问题是,我如何一次性跳出两个嵌套的循环?例如,如何我才能检查字符串中的字符对,然后在我找到一对相等的字符对时停止?经典的方式是写两个嵌套循环,遍历该字符串的索引:
s = "a string to examine"
for i in range(len(s)):
for j in range(i+1, len(s)):
if s[i] == s[j]:
answer = (i, j)
break # How to break twice???
这里,我们使用两个循环来生成两个我们想要检查的索引。当我们找到正在寻找的条件时,我们想退出这两个循环。
有几个常见的答案。但我不大喜欢它们:
- 将循环放进一个函数中,然后在函数中返回以退出循环。这个答案不尽人意,因为循环也许是重构成新函数的一个自然而然的地方,另外,也许在循环中,你需要访问其他本地变量。
- 抛出一个异常,然后在双循环的外部捕获它。这是把异常当成goto来用。这里并没有异常条件,你只是超距利用异常操作。
- 使用布尔变量来标记循环结束,然后在外部循环检查该变量,从而执行第二次退出。这是一个低技术解决方案,可能适用于某些情况,但大多数只是带来额外的噪声和标记。
我的首选答案,也就是那个我在PyCon 2013演讲上提到的(Loop Like A Native)那个,是把双循环变成单循环,然后只适用一个break。
这需要把多一点的工作放到循环里,但是,这对于抽象迭代是一个好的实践。这是Python非常擅长的一件事,但也非常容易把Python当成一个能力较差的语言来用,并且不利用现有的循环抽象。
让我们再来考虑这个问题。这真的是两个循环吗?在写任何代码之前,再听一听该英文描述:
如何我才能检查字符串中的字符对,然后在我找到一对相等的字符对时停止?(How can I examine pairs of characters in a string, stopping when I find an equal pair?)
在该描述中,我并未听到两个循环。是一个字符对上的单循环。因此,让我们这样写:
def unique_pairs(n):
"""Produce pairs of indexes in range(n)"""
for i in range(n):
for j in range(i+1, n):
yield i, j
s = "a string to examine"
for i, j in unique_pairs(len(s)):
if s[i] == s[j]:
answer = (i, j)
break
这里,我们写了一个生成器来生成所需的索引对。现在,我们的循环是在字符对之上的单循环,而不是通过索引的双循环。仍然有双循环,但是抽取到unique_pairs生成器中。
这让我们的代码漂亮地匹配了我们的话。注意,我们无需写两遍len(s),这是原始代码需要重构的另一个迹象。如果在其他地方,我们发现我们想要像这样进行迭代,那么可以重用unique_pairs生成器,虽然,可重用不是写一个函数的必备条件。
我知道这种技术似乎是外来的。但它确实是最好的解决方案。如果你还是觉得被绑在双循环上,那么多想想你是如何想像你的程序的结构的。不管你相信与否,你正在试图一次打破两个循环,这意味着,在某种意义上,它们是一回事,而不是两个。将第二个隐藏到一个生成器中,你就可以以实际上认为的那样来重构你的代码。
Python有用于抽象的强大的工具,其中包括生成器和其他用于抽象迭代的技术。如果你想要了解更多的话,我的Loop Like A Native演讲有更多详细信息(和一个令人震惊的笑话)。