Нужна помощь в улучшении кода Python с использованием списков - PullRequest
3 голосов
/ 09 июля 2009

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

# Compute a list of the mileage differences between each oil change.
diffs = [j - i for i, j in zip(oil_changes[:-1], oil_changes[1:])]

# Use the average difference between oil changes to estimate the next change.
next_oil = oil_changes[-1] + sum(diffs) / len(diffs)

Код дает правильный ответ (проверял математику вручную), но он пока не выглядит достаточно Pythonic. Я делаю много ненужного копирования оригинального списка в первой строке? Я чувствую, что есть гораздо лучший способ сделать это, но я не знаю, что это такое.

Ответы [ 5 ]

9 голосов
/ 09 июля 2009

Как указывали другие ответы, вам не нужно беспокоиться, если ваш список oil_changes не слишком длинный. Однако, как поклонник «потоковых» вычислений, я думаю, что интересно отметить, что itertools предлагает все инструменты, необходимые для вычисления значения next_oil в пространстве O (1) (и времени O (N) конечно! -) независимо от того, какой большой N, то есть len(next_oil), становится.

izip само по себе недостаточно, потому что это только немного уменьшает мультипликативную константу, но оставляет ваши требования пространства как O (N). Основная идея довести эти требования до O (1) состоит в том, чтобы соединить izip с tee - и избегать понимания списка, которое в любом случае будет O (N) в пространстве, в пользу хорошего простого старого - вылепленная петля! -). Вот идет:

  it = iter(oil_changes)
  a, b = itertools.tee(it)
  b.next()
  thesum = 0
  for thelen, (i, j) in enumerate(itertools.izip(a, b)):
    thesum += j - i
  last_one = j
  next_oil = last_one + thesum / (thelen + 1)

Вместо того, чтобы брать кусочки из списка, мы берем итератор, делаем его (создаем два его независимо продвигаемых клона) и продвигаем, один раз, один из клонов, b. tee занимает пробел O (x), где x - максимальная абсолютная разница между продвижением различных клонов; здесь продвижение двух клонов отличается не более чем на 1, поэтому необходимое место явно равно O (1).

izip выполняет поочередное «сжатие» двух итераторов клонов с небольшим перекосом, и мы одеваем их в enumerate, чтобы мы могли отслеживать, сколько раз мы проходим цикл, т.е. длина итерируемой части, по которой мы итерируем (нам нужно +1 в конечном выражении, потому что enumerate начинается с 0! -). Мы вычисляем сумму с помощью простого +=, что хорошо для чисел (sum еще лучше, но не будет отслеживать длину! -).

Заманчиво после цикла использовать last_one = a.next(), но это не сработает, потому что a фактически исчерпан - izip продвигает свой аргумент итерируемо слева направо, поэтому он продвинулся a в последний раз перед он понимает, что b кончено! -). Это нормально, потому что переменные цикла Python НЕ ограничены по объему самим циклом - после цикла j все еще имеет значение, которое было в последний раз извлечено путем продвижения b до того, как izip сдался (точно так же, как thelen все еще имеет последнее значение счетчика, возвращаемое enumerate). Я все еще называю значение last_one вместо того, чтобы использовать j непосредственно в конечном выражении, потому что я думаю, что оно более четкое и более читаемое.

Так что вот оно - надеюсь, это было поучительно! -) - хотя для решения конкретной проблемы, которую вы поставили на этот раз, это почти наверняка будет излишним. У нас, итальянцев, есть древняя пословица - «Impara l'Arte, e mettila da parte!» ... «Изучай искусство, а потом откладывай его в сторону» - что, я думаю, вполне применимо здесь: это хорошая вещь для изучения продвинутые и сложные способы решения очень сложных проблем, в случае, если вы когда-либо встречаете их, но при этом все, что вам нужно, - это стремление к простоте и прямоте в гораздо более распространенном случае простых, обычных проблем - не применять передовые решения, которые, скорее всего, выиграли не нужно! -)

9 голосов
/ 09 июля 2009

Попробуйте это:

assert len(oil_changes) >= 2
sum_of_diffs = oil_changes[-1] - oil_changes[0]
number_of_diffs = len(oil_changes) - 1
average_diff = sum_of_diffs / float(number_of_diffs)
3 голосов
/ 09 июля 2009

Пакет itertools предоставляет дополнительные функции в стиле генератора. Например, вы можете использовать izip вместо zip для экономии памяти.

Возможно, вы также можете написать average функцию, чтобы вы могли превратить diffs в генератор вместо понимания списка:

from itertools import izip

def average(items):
    sum, count = 0, 0

    for item in items:
        sum   += item
        count += 1

    return sum / count

diffs = (j - i for i, j in izip(oil_changes[:-1], oil_changes[1:])
next_oil = oil_changes[-1] + average(diffs)

Кроме того, вы можете изменить определение diffs на:

diffs = [oil_changes[i] - oil_changes[i-1] for i in xrange(1, len(oil_changes))]

Я не знаю, это не очень большое улучшение. Ваш код довольно хорош как есть.

2 голосов
/ 09 июля 2009

Я делаю много ненужного копирования оригинального списка в первом линия

Технически, да. Реально нет. Если вы не меняли масло буквально миллионы раз, штраф за скорость вряд ли будет значительным. Вы можете изменить zip на izip, но вряд ли оно того стоит (а в python 3.0 zip эффективно равно izip).

Вставьте эту старую цитату Кнута здесь.

(вы также можете заменить oil_changes[:-1] просто oil_changes, так как zip() в любом случае усекает до длины самой короткой входной последовательности)

2 голосов
/ 09 июля 2009

Кажется, хорошо, правда. Не все просто (у вас есть несколько шагов в другом простом вычислении, независимо от того, как вы его создаете). Существуют варианты уменьшения количества копий, например, использование itertools.islice и itertools.izip, но (кроме izip) дополнительные шаги в коде только усложнят его. Не все должно быть пониманием списка, но иногда это суждение. Что выглядит чище для вас? Что следующий парень, который читает это, поймет лучше? Что вы поймете, когда вернетесь, чтобы исправить эту ошибку через три месяца?

...