2009-07-09 2 views
3

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

# 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. Я делаю много ненужного копирования исходного списка в первой строке? Я чувствую, что есть намного лучший способ сделать это, но я не знаю, что это.

ответ

9

Как и другие ответы отметили, вы действительно не нужно беспокоиться, если ваш 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!» ... «Изучите искусство, а затем отложите его» - что, я думаю, здесь вполне применимо: хорошо учиться передовые и сложные способы решения очень сложных проблем, если вы когда-либо встречаете их, но для всего, что вам нужно для простоты и прямоты в гораздо более распространенном случае простых обычных проблем - не применять передовые решения, которые, скорее всего, выиграли Не нужно! -)

+0

Всегда есть компромисс. Ваш код является самым медленным среди ответов здесь в соответствии с timeit. –

+0

Для коротких списков это может быть; попробуйте различные подходы с несколькими миллионами элементов в oil_changes, скажем ... ;-) –

+1

Возможно, стоит отметить, что конструкция tee + next + izip обычно абстрагируется как попарно(), подробности см. в документации itertools. С другой стороны, использование переменной for вне цикла (хотя принято как интерпретатором, так и сообществом Python) является IMHO довольно уродливым. Тем не менее, более функциональный (в смысле FP) решение будет труднее для новичков понять и, вероятно, не рассматривается как «Pythonic» (так как он будет использовать так ненавистный сокращение/foldl). – tokland

2

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

3

Пакет 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))] 

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

+0

Интересно, что ваше альтернативное определение diffs приводит к самому быстрому времени выполнения среди ответов здесь (за исключением ответа Джона Мачина, конечно). –

+0

может в среднем быть суммой (штук)/len (items), если len (items)> 0? – Martlark

9

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

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) 
+0

Это, безусловно, лучший способ получить мой ответ, но тогда я бы не узнал ничего о List Comprehensions. +1 в любом случае. :-) –

+2

Изучение техники X должно включать не использование техники X, когда это не оправдано - см. Итальянскую пословицу Алекса. Обратите внимание, что тот факт, что в ответе используется только первое и последнее расстояние, указывает, что предсказательная способность среднего арифметического различий может быть не столь велика. Вот лучший пример, чтобы попробовать свои методы: вычислить экспоненциальную скользящую среднюю (более свежие результаты имеют больший вес, чем предыдущие) - он не может быть оптимизирован в однострочный. –

2

Могу ли я делать много ненужной копирования из первоначального списка в первой строке ?

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

Вставьте это old quote by Knuth здесь.

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

 Смежные вопросы

  • Нет связанных вопросов^_^