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

Я часто заканчиваю тем, что пишу немного кода дважды при использовании циклов. Например, просматривая курс информатики 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 строк кода. Это обычное дело или результат моего мышления / кода. Что я должен делать?

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


person mowwwalker    schedule 22.06.2012    source источник
comment
пожалуйста, используйте if some_string:, чтобы проверить, что some_string не пусто   -  person jfs    schedule 22.06.2012
comment
if not l является избыточным. l - плохая репутация. most_reps можно было бы назвать max_count, чтобы прояснить отношение к count. i - ›current, prv -› last   -  person jfs    schedule 22.06.2012


Ответы (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 (примеры прямо здесь).
  • Кроме того, компиляторы также могут обнаруживать и изменять / перемещать блоки по вашему коду.
  • Если есть языковая функция / библиотека, которая делает ваш код быстрым и в то же время читаемым, используйте его. (Другие ответы здесь указывают на то, что предлагает python :))
person UltraInstinct    schedule 22.06.2012
comment
Первое правило оптимизации: не надо. Второй - еще нет. Заставьте это работать, исправьте, сделайте это быстро. - person jfs; 22.06.2012

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

person Wes    schedule 22.06.2012
comment
Я подумывал написать версию выражения для понимания списка / генератора, но потом решил, что это будет для него слишком запутанным. - person Wes; 22.06.2012
comment
единственный genexpr в моем коде - заменить ваш len(list(xs)) (который без причины потребляет память) на sum(1 for _ in xs). Сложнее всего понять groupby() все остальное просто по сравнению. - person jfs; 22.06.2012

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

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

person jfs    schedule 22.06.2012

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

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

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

person srgerg    schedule 22.06.2012
comment
А вот из-за дополнительных проверок я написал иначе. Я полагаю, что еще одно сравнение в конце цикла дешевле, чем постоянное сравнение. - person mowwwalker; 22.06.2012

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

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» на своем и каждый раз запускать в нем код. (Это ответ Сергерга.)

Однако это может быть непросто для некоторых видов проверок. Для подсчета слов вам нужно добавить некоторую дополнительную логику, чтобы избежать накопления мусора из «частичных» подпоследовательностей, которые вы обрабатываете. Вот код, который это делает:

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})
person Blckknght    schedule 22.06.2012
comment
Для лучшего разделения проблем (это должно присутствовать даже (или особенно), если код предназначен для начинающих) код подсчета слов и код сегментации текста должны быть разделены, как в вашем последнем примере (Counter counts, genexpr генерирует слова). - person jfs; 22.06.2012

Итераторы предоставляют удобный способ разбивать циклы:

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

Во многих языках есть похожая концепция.

person Vaughn Cato    schedule 22.06.2012