Избегать повторения кода после цикла?

Я часто заканчиваю тем, что пишу немного кода при использовании циклов. Например, перейдя по курсу "Удипс", я написал код (для функции, чтобы найти наиболее последовательно повторяющийся элемент):

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 строк кода. Является ли это общим, или результатом того, как я думаю/код. Что мне делать?

edit: Аналогичным образом, в примере на основе форматирования строк:

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

edit: Экзамен, из которого это было написано, был написан для студентов курса, у которых не ожидается каких-либо внешних знаний о Python; только то, чему учили в предыдущих подразделениях. Хотя у меня есть предыдущий опыт работы на Python, я стараюсь придерживаться этих ограничений, чтобы максимально использовать курс. Преподавали такие вещи, как str.split, списки и множество основ Python, но ничего не было на импорте - особенно не такие вещи, как groupby. Это, как говорится, как это должно быть написано без каких-либо языковых особенностей, которые, вероятно, не будут преподаваться в курсе введения программирования.

Ответы

Ответ 1

Поскольку вы отметили language-agnostic, я вижу, что вы не будете очень заинтересованы в специфичных для python материалах, которые вы могли бы использовать, чтобы сделать ваш код эффективным, компактным и читаемым. По той же причине я не собираюсь показывать, как красивый код может быть написан на питоне.

В некоторых случаях, когда дополнительный if в конце можно избежать в зависимости от вашего алгоритма, но в большинстве случаев ему нравится "Если он существует, он должен быть значительным и/или эффективным". Я не знаю, как работает интерпретатор python, но в скомпилированных языках, таких как C/С++/etc. компилятор выполняет различные виды оптимизации цикла, включая перемещение if-блоков из цикла, если он делает то же самое.

Я побежал и сравнил время выполнения различных фрагментов:

  • @JFSebastian - 8.9939801693
  • @srgerg - 3.13302302361
  • ваш - 2.8182990551.

Это не обобщение, что конечное if дает вам лучшее время. Я хочу сказать: просто следуйте своему алгоритму и попытайтесь его оптимизировать. Нет ничего плохого в if в конце. Вероятно, альтернативные решения дороги.

О втором примере, который вы положили: проверка tmp == '' выполняется для обеспечения возврата только непустых строк. Это на самом деле является дополнительным условием для вашего алгоритма разделения. В любом случае вам понадобится дополнительный rtn.append после цикла, потому что все еще есть что-то за последним разделителем. Вы всегда можете нажать условие if внутри цикла, например if curCharIndex == lastIndex: push items to list, которое будет выполняться на каждой итерации, а также в виде одного и того же случая.

Мой короткий ответ:

  • Ваш код так же эффективен, как ваш алгоритм, который вы имеете в виду.
  • В конце концов if встречаются во многих случаях - не нужно беспокоиться о них, они могут сделать код более эффективным, чем альтернативные подходы, без такого, если (примеры здесь).
  • Кроме того, компиляторы также могут определять и изменять/перемещать блоки вокруг вашего кода.
  • Если есть языковая функция/библиотека, которая делает ваш код быстрым и в то же время читаемым, используйте его. (Другие ответы здесь указывают, что предлагает python:))

Ответ 2

Посмотрите на реализацию 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 и использовать его.

Ответ 3

Язык-агностическая техника, чтобы избежать повторения условия после цикла, заключается в добавлении значений дознания во входные данные, например, если delimiter добавлено к концу string, тогда условие не требуется в split_by(). Канонический пример: в алгоритме линейного поиска игла может быть добавлена ​​к стоге сена, чтобы избежать завершения проверки последовательности.

Другой вариант - делегировать некоторую работу отдельной функции, например, одна функция подсчитывает количество повторений, другая находит максимум, как в longest_repetition():

from itertools import groupby

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

Если повторяющийся код тривиален; это может не стоить усилий.

Ответ 4

Нередко возникает необходимость перепроверить условие в конце цикла, который также проверяется внутри цикла. Если вы готовы пожертвовать небольшой эффективностью, один из способов избежать повторной проверки - перепроверить ее внутри цикла. Например:

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 чаще, чем нужно, но избегает необходимости проверять его снова после цикла.

К сожалению, такое изменение не будет применимо ни при каких обстоятельствах.

Ответ 5

Я думаю, что есть три общих подхода, которые могут помочь вам избежать повторения кода в конце цикла. Для всех трех я собираюсь использовать примерную проблему, немного отличающуюся от вашей, считая слова в строке. Здесь версия по умолчанию, которая, как и ваш код, повторяет некоторую логику в конце цикла:

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()))

Вывод, если задан подходящий питонический текст (он одинаковый для всех четырех версий countWords):

>>> text = """Well, there 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})

Ответ 6

Итераторы обеспечивают хороший способ разбить циклы:

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

У многих языков есть аналогичная концепция.