循环后避免重复代码?

时间:2012-06-22 04:01:03

标签: python language-agnostic code-organization

在使用循环时,我经常最终写两次代码。例如,在浏览Udacity计算机科学课程时,我编写了代码(用于查找最顺序重复元素的函数):

def longest_repetition(l):
    if not l:
        return None
    most_reps = count = 0 
    longest = prv = None
    for i in l:
        if i == prv:
            count += 1
        else:
            if count > most_reps:
                longest = prv
                most_reps = count
            count = 1
        prv = i
    if count > most_reps:
        longest = prv
    return longest

在这种情况下,如果计数大于先前重复的元素,我会检查两次。当当前元素与上一个元素不同以及当我到达列表末尾时,都会发生这种情况。

在按字符解析字符串时,我也遇到过这种情况。还有几次代码大约有5行代码。这是常见的,还是我思考/编码的结果。我该怎么办?

编辑:同样,在一个人为的字符串拆分示例中:

def split_by(string, delimeter):
    rtn = []
    tmp = ''
    for i in string:
        if i == delimeter:
            if tmp != '':
                rtn.append(tmp)
                tmp = ''
        else:
            tmp += i
    if tmp != '':
        rtn.append(tmp)
    return rtn

编辑:这来自的考试是为课程的学生编写的,他们不需要任何Python的外部知识;只有以前单位教过的东西。虽然我确实有过Python的经验,但我仍然试图遵循这些限制来充分利用这个课程。像str.split,列表和Python的许多基础知识这样的东西都被教过,但是还没有任何关于进口的东西 - 特别是像groupby这样的东西。话虽这么说,如何在没有任何可能不会在编程入门课程中教授的语言特征的情况下编写。

6 个答案:

答案 0 :(得分:6)

由于您标记了language-agnostic,我发现您对可以用来使代码高效,紧凑和可读的python特定内容不太感兴趣。出于同样的原因,我不会展示在python中编写代码的漂亮程度。

在某些情况下,根据您的算法,最终可以避免额外的if,但大多数情况下它就像“如果它存在,它应该是重要的和/或有效的”。我不知道python解释器是如何工作的,但是在C / C ++ /等编译语言中。编译器执行各种循环优化,包括将if块移出循环,如果它做同样的事情。

我跑了并比较了各种片段的运行时间:

  • @JFSebastian - 8.9939801693
  • @srgerg - 3.13302302361
  • 你的 - 2.8182990551。

尾随if给你最好的时间并不是一种概括。我的观点是:只需按照您的算法,并尝试优化它。最后if没有任何问题。可能替代解决方案很昂贵。

关于您放入的第二个示例:检查tmp == ''已完成以确保仅返回非空字符串。这实际上是分裂算法的一种附加条件。在任何情况下,循环后都需要一个额外的rtn.append,因为还有一些超出最后一个分隔符的东西。你总是可以在循环中推送一个if条件,就像if curCharIndex == lastIndex: push items to list一样,它会在每次迭代中执行,并且它会再次出现相同的情况。

我的答案简短:

  • 您的代码与您的算法一样高效。
  • 在很多情况下遇到最后的if - 无需担心它们,它们可能使代码比没有这样的if的替代方法更有效(示例就在这里)。< / LI>
  • 此外,编译器还可以发现和修改/移动代码周围的块。
  • 如果有一个语言功能/库使您的代码快速且同时可读,请使用它。 (其他答案在这里指出python提供的内容:))

答案 1 :(得分:5)

查看itertools.groupby的实现,它几乎完全符合您的要求。 http://docs.python.org/library/itertools.html#itertools.groupby

以下是使用所述代码的算法:

from itertools import groupby

string = "AAABBCCDDDD"

maximum = 0
max_char = ""

for i in groupby(string):
    x, xs = i
    n = len(list(xs))
    if n > maximum:
        max_char = x
        maximum = n

print max_char

我考虑将来编写这样的算法的建议是尽量不要在一个函数中做所有事情。考虑解决您尝试解决的问题的较小函数,例如“将序列中相等项目的每个序列分组为较小的序列”。

当然,它也不一定是上述算法中的字符 - 它可以是任何可分组的。

编辑:为了响应OP的编辑,我想你不会在类设置中使用/了解像itertools这样的库,但我并不是说你应该依赖外部库,而是更多你应该通过将问题分成更小的子问题来考虑问题。因此,在这种情况下,您将实现自己的groupby并使用它。

答案 2 :(得分:5)

在循环之后避免重复条件的语言不可知技术是将sentinel值附加到输入数据,例如,如果delimiter附加到string的末尾,那么split_by()中的条件不是必需的{1}}。典型示例:在线性搜索算法中,可以将针附加到大海捞针以避免序列检查结束。

另一个选择是将一些工作委托给一个单独的函数,例如,一个函数计算重复次数,另一个函数在longest_repetition()中找到最大值:

from itertools import groupby

def longest_repetition(iterable):
    return max(groupby(iterable), key=lambda x: sum(1 for _ in x[1]))[0]

如果重复的代码是微不足道的;这可能不值得。

答案 3 :(得分:2)

在循环内部进行检查的循环结束时需要重新检查条件并不罕见。如果你准备牺牲一点效率,避免重复检查的一种方法是在循环内过度检查它。例如:

def my_longest_repetition(l):
    if not l:
        return None
    most_reps = count = 0
    longest = prv = None
    for i in l:
        count = (count + 1) if i == prv else 1
        if count > most_reps:
            longest = prv
            most_reps = count
        prv = i
    return longest

此代码检查count > most_reps的次数超出了需要,但避免在循环后再次检查它。

不幸的是,这种变化并不适用于所有情况。

答案 4 :(得分:2)

我认为有三种通用方法可以帮助您避免在循环结束时重复代码。对于所有这三个,我将使用与您自己略有不同的示例问题,计算字符串中的单词。这是一个“默认”版本,就像你的代码一样,在循环结束时重复一些逻辑:

from collections import Counter

def countWords0(text):
    counts = Counter()
    word = ""

    for c in text.lower():
        if c not in "abcdefghijklmnopqrstuvwxyz'-":
            if word:
                counts[word] += 1
            word = ""
        else:
            word += c

    if word:
        counts[word] += 1 # repeated code at end of loop

    return counts

第一种方法是在每个字符之后执行(部分)“子序列结束”处理,以便如果序列在该字符之后立即结束,则簿记是正确的。在您的示例中,您可以消除您的“else”条件,并且每次都在其中运行代码。 (这是sergerg的答案。)

但是对于某些类型的检查来说,这可能并不容易。对于计算单词,您需要添加一些额外的逻辑,以避免累积您处理的“部分”子序列。这是执行此操作的代码:

def countWords1(text):
    counts = Counter()
    word = ""

    for c in text.lower():
        if c not in "abcdefghijklmnopqrstuvwxyz'-":
            word = ""
        else:
            if word:
                counts[word] -= 1 # new extra logic
            word += c
            counts[word] += 1 # this line was moved from above

    return counts + Counter() # more new stuff, to remove crufty zero-count items

第二个选项是将序号值附加到序列的末尾,这将触发所需的“子序列结束”行为。如果你需要避免哨兵污染你的数据(特别是数字之类的东西),这可能会很棘手。对于最长的连续子序列问题,您可以添加任何不等于序列中最后一项的值。 None可能是个不错的选择。对于我的计数单词示例,非单词字符(例如换行符)将执行:

def countWords2(text):
    counts = Counter()
    word = ""

    for c in text.lower() + "\n": # NOTE: added a sentinel to the string!
        if c not in "abcdefghijklmnopqrstuvwxyz'-":
            if word:
                counts[word] += 1
            word = ""
        else:
            word += c

    # no need to recheck at the end, since we know we ended with a space

    return counts

第三种方法是更改​​代码的结构,以避免迭代可能意外结束的序列。您可以使用生成器预处理序列,就像使用groupby中的itertools的其他答案一样。 (当然,如果您必须自己编写生成器函数,可能会遇到类似的问题。)

对于我的单词计数示例,我可以使用re模块中的正则表达式来查找单词:

from re import finditer

def countWords3(text):
    return Counter(match.group() for match in
                   finditer("[\w'-]+", text.lower()))

输出时,给出适当的Pythonic文本(对于所有四种版本的countWords都是相同的):

>>> text = """Well, there's egg and bacon; egg sausage and bacon;
              egg and spam; egg bacon and spam; egg bacon sausage and spam;
              spam bacon sausage and spam; spam egg spam spam bacon and spam;
              spam sausage spam spam bacon spam tomato and spam;
              spam spam spam egg and spam; spam spam spam spam spam spam
              baked beans spam spam spam; or Lobster Thermidor a Crevette
              with a mornay sauce served in a Provencale manner with shallots
              and aubergines garnished with truffle pate, brandy and with a
              fried egg on top and spam."""

>>> countWords0(text)
Counter({'spam': 28, 'and': 12, 'egg': 8, 'bacon': 7, 'sausage': 4, 'a': 4,
         'with': 4, 'well': 1, 'lobster': 1, 'manner': 1, 'in': 1, 'top': 1,
         'thermidor': 1, "there's": 1, 'truffle': 1, 'provencale': 1,
         'sauce': 1, 'brandy': 1, 'pate': 1, 'shallots': 1, 'garnished': 1,
         'tomato': 1, 'on': 1, 'baked': 1, 'aubergines': 1, 'mornay': 1,
         'beans': 1, 'served': 1, 'fried': 1, 'crevette': 1, 'or': 1})

答案 5 :(得分:1)

迭代器提供了一种分解循环的好方法:

def longest_repetition(l):
  i=iter(l)
  n=next(i,None)
  longest=None
  most_reps=0
  while n is not None:
    p=n
    count=0
    while p==n:
      n=next(i,None)
      count+=1
    if count>most_reps:
      most_reps=count
      longest=p
  return longest

许多语言都有类似的概念。