Эта статья описывает различные "хитрости", которые позволяют
увеличить скорость Вашей питон-программы. Если информация поступает
от кого-то еще, я стараюсь указывать источник.
Питон несколько раз сильно менялся с тех пор, как я ( Skip
Montanaro -- прим. j2a) написал свою страничку "быстрый питон"
примерно в 1996, так что с тех времен кое-что изменилось. Я перешел
на вики и надеюсь, это поможет поддерживать страничку в актуальном
состоянии.
Замечание: Вы всегда должны проверять описанные рецепты в своем
приложении и в используемой версии Питон, а не слепо доверять,
что "это быстрее чем вон то".
Также с тех пор появились некоторые новые оригинальные пакеты, такие
как: Pyrex, Psyco, Weave и [26] PyInline, которые могут
очень прилично увеличить скорость Вашего приложения, позволяя
критичный к скорости код реализовывать на C.
Оптимизируй то, что нужно
Вы можете узнать, что программа медленна, только если она правильная.
Только после того, как программа выдала корректный результат, можно
увидеть, что правильная программа -- медленна.
Когда обнаружено, что программа медленна, профилировка поможет
определить части программы, где теряется больше всего времени. Полный
но быстрый (для запуска) юнит-тест впоследствии поможет
проконтролировать, что оптимизация не "повредила" корректности
программы. Кратко:
1. Сделай правильно
2. Проверь, правильно ли
3. Профилируй
4. Оптимизируй
5. Повтори с шага 2
Некоторые оптимизации относятся к хорошему стилю программирования,
которому нужно также учиться, как и самому языку. Например, выносить
выражения, не зависящие от переменных в цикле, за пределы цикла.
Выбирайте правильные структуры данных
To bee done... (пока данный раздел не описан в оригинале -- прим. j2a)
Сортировка
Сортировка списков базовых объектов Питона в целом весьма эффективна.
Метод сортировки списков в качестве опционального аргумента берет
функцию сравнения, так что порядок и поведение сортировки могут быть
изменены. Это достаточно удобно, хотя может сильно замедлить Вашу
сортировку, поскольку функция сравнения будет вызываться много раз.
В Питоне версии 2.4, Вы можете передавать встроенной функции
сортировки аргумент "key", и похоже, по производительности это будет
наилучший вариант.
Если у вас старые версии Питона (ниже 2.4), используйте следующий
совет Гвидо ван Россума:
альтернативный способ увеличить скорость сортировки -- создать список
кортежей, чей первый элемент ключ сортировки такой, что нужным образом
сортируется стандартной сортировкой, а второй элемент -- элемент
оригинального списка. Это также называется преобразованием Шварца,
или [28] decorate-sort-undecorate (DSU).
Например, полагаем, что у Вас есть список кортежей, который Вы хотите
отсортировать по n-ому полю каждого кортежа. Следующая функция делает
это:
def sortby(somelist, n):
nlist = [(x[n], x) for x in somelist]
nlist.sort()
return [val for (key, val) in nlist]
(списковые выражения появились начиная с версии 2.0 питона -- прим.
j2a)
Точно такое же поведение для текущего списка (сортировка "на месте")
также достижимо следующим образом:
def sortby_inplace(somelist, n):
somelist[:] = [(x[n], x) for x in somelist]
somelist.sort()
somelist[:] = [val for (key, val) in somelist]
return
Совет от Tim Delaney:
Начиная с Питон 2.3 сортировка стабильная (точнее, она стабильна
в CPython 2.3 и наверняка будет стабильна в 2.4).
В Питоне 2.4 добавлен дополнительный параметр "key", который легко
использовать:
Учтите, что исходный элемент никогда не используется в сортировке,
только возвращаемый ключ, это эквивалентно:
nlist = [(x[i], i, x) for (i, x) in enumerate(nlist)]
nlist.sort()
nlist = [val for (key, index, val) in nlist]
Конкатенация строк
Внимание: данный совет следует уточнить для Python 2.5, в нем
конкатенация строк (не unicode) быстрая.
Строки в Питоне неизменяемы. Этот факт часто упускают из виду
начинающие Питон-программисты. Неизменяемость имеет как преимущества,
так и недостатки. Плюсы: строки могут использоваться в качестве ключей
в словарях и отдельные копии могут быть разделены между различными
переменных (Питон автоматически разделяет одно- или двух-символьные
строки). Минусы: Вы не можете сделать что-нибудь вроде "поменять
все 'а' на 'б'" в текущей строке. Вместо этого Вы должны создать новую
строку с желаемыми свойствами. Последовательные копирования могут
привести к приличной потере производительности в Питон-программ --
см. [29] ConcatenationTestCode.
Старайтесь избегать такого кода:
s = ""
for substring in list:
s += substring
Вместо этого используйте s = "".join(list).
Другая очень распространенная и катастрофичная (по производительности)
ошибка совершается при построении длинных строк. Так что. если
Вы генерируете строки последовательно, вместо
s = ""
for x in list:
s += some_function(x)
используйте
slist = [some_function(elt) for elt in somelist]
s = "".join(slist)
Избегайте
out = "<html>" + head + prologue + query + tail + "</html>"
Вместо этого, лучше пишите так:
out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)
Более того, для читабельности (не имеет ничего общего
с эффективностью, разве что Вашей как программиста), используйте
словарь подстановки:
out = "<html>%(head)s%(prologue)s%(query)s%(tail)s</html>" % locals()
Последние два варианта намного быстрее, особенно ощутимо когда
суммируется по многочисленным вызовам CGI-скриптов, к тому же их
гораздо проще изменять.
Циклы
Питон поддерживает несколько конструкций циклов. Цикл for наиболее
часто используемый. Он "пробегает" по всем элементам
последовательности, присваивая каждый из них переменной цикла. Если
тело цикла простое, то накладные расходы (оверхед) интерпретатора
на сам цикл for могут составлять существенную часть всех накладных
расходов. Это то место, где функция map весьма полезна. Вы можете
считать, что map это for, переписанный на C. Единственное ограничение,
что "тело цикла" map должно быть вызовом функции. Списковые
выражения, помимо более понятного синтаксиса, также быстры или еще
быстрей, чем map.
Ниже приведен пример. Вместо цикла по списку слов и преобразования
их в верхний регистр:
newlist = []
for word in oldlist:
newlist.append(word.upper())
Вы можете использовать map для того чтобы "перенести" цикл
из интерпретатора в скомпилированный C-код:
newlist = map(str.upper, oldlist)
Списковые выражения были добавлены в Питон 2.0. Они дают синтаксически
более компактный и эффективный способ написания подобного цикла:
newlist = [s.upper() for s in oldlist]
Выражения-генераторы были добавлены в версии 2.4 Питона. Это функции,
более-или-менее как списковые выражения или как map, но без накладных
расходо на генерацию полного списка сразу. Вместо этого,
они возвращают объект генератора, по которому может проходить итерация
пошагово:
newlist = (s.upper() for s in oldlist)
Какой метод применять зависит от используемой версии Питона
и характеристик исходных данных.
Гвидо ван Россум написал более подробное(и более лаконичное)
объяснение оптимизации циклов в [31] своем эссе, крайне рекомендуется
к прочтению. (Существует два перевода этого эссе: ~1, ~2 --
прим. j2a )
Избегая точек...
Полагаете, что Вы не можете использовать map или списковые выражения?
Возможны, вы слишком "привыкли" к циклу for. Пример цикла for выше
имеет еще одно неэффективное место. Как newlist.append
так и word.upper являются ссылками на функции и вычисляются заново
при каждом вызове (т.е. при каждой итерации цикла). Оригинальный цикл
можно переписать так:
upper = str.upper
newlist = []
append = newlist.append
for word in list:
append(upper(word))
Эта техника должна использоваться с осторожностью. Она дает ощутимые
трудности сопровождения если цикл большой. В случае если Вы весьма
хорошо разбираетесь в улучшаемом коде, то можете попробовать заменить
append и upper вышеуказанным способом.
Локальные переменные
И последнее, чем можно увеличить скорость не-map версии цикла for --
это использовать локальные переменные там где только можно. Если
представить вышеуказанный цикл функцией, то append и upper становятся
локальными переменными. Доступ Питона к локальным переменным гораздо
более эффективен, чем доступ к глобальным переменным.
def func():
upper = str.upper
newlist = []
append = newlist.append
for word in words:
append(upper(word))
return newlist
В то время, когда я ( Skip Montanaro -- прим. j2a) писал
эти строки, я использовал 100МГЦ Pentium и BSDi. И я получил следующие
времена на преобразование списка слов из /usr/share/dict/words (38,470
на текущий момент):
Версия Время, с
Оригинальный цикл 3.47
Убрав точки 2.45
Локальные переменные+без точек 1.79
Используя функцию map 0.54
Инициализация элементов словаря
Допустим, что Вы составляете словарь частоты встречаемых слов и Вы
уже разбили Ваш текст на слова. Наверное, Вы захотите сделать что-то
подобное:
wdict = {}
for word in words:
if word not in wdict:
wdict[word] = 0
wdict[word] += 1
Кроме самого первого раза, при каждой итерации word, выражение if
ложно. Если Вы подсчитываете большое количество слов, есть
вероятность, что большинство слов встречаются более одного раза.
В ситуации, когда инициализация значения происходит один раз и велика
вероятность, что значение будет встречаться много раз, выгоднее
использовать try:
wdict = {}
for word in words:
try:
wdict[word] += 1
except KeyError:
wdict[word] = 1
Важно отметить, что перехватывать нужно только ожидаемое KeyError
исключение, а не оставлять просто except, чтобы не "словить"
исключение, которые не обрабатывается внутри try/except.
Третий вариант стал возможен в версии Питона 2.0 и старше. Словари
теперь имеют метод get(), который возвращает значение по умолчанию
если искомый ключ не найден в словаре. Это упрощает цикл:
wdict = {}
get = wdict.get
for word in words:
wdict[word] = get(word, 0) + 1
Когда я ( Skip Montanaro -- прим. j2a) писал этот раздел, были
явные ситуации, когда один из первых двух вариантов был быстрее.
Похоже, что сейчас все три подхода дают примерно одинаковую
производительность (примерно с разницей 10% относительно друг друга),
более или менее зависимо от свойств списка слов.
Если данные, хранимые в слваре -- объекты или (изменяемые) списки,
то Вы также можете использовать метод dict.setdefault, т.е.:
...
wdict.setdefault(key, []).append(new_element)
Такой подход избавляет от необходимости дважды искать значение
в словаре по ключу.
Накладные расходы на import
import может быть выполнен в любом месте. Часто это используется
внутри функции чтобы скрыть из глобального пространства имен и/или
сократить время начального запуска программы. Хотя интерпретатор
Питона оптимизирован на то, чтобы не импортировать один и тот
же модуль несколько раз, в некоторых ситуациях последовательные
импорты могут сильно повлиять на производительность.
Рассматриваем следующие фрагменты кода (изначально от Greg McFaralne,
мне так кажется, -- я нашел этот код "безымянным" в [37]
comp.lang.python и позже приписал Greg'у авторство, получив данные
из другого источника)
def doit1():
import string ###### импорт внутри функции
string.lower('Python')
for num in range(100000):
doit1()
или:
import string ###### импорт вне функции
def doit2():
string.lower('Python')
for num in range(100000):
doit2()
doit2 выполняется гораздо быстрее чем doit1, даже если ссылка
на модуль string глобальна в doit2. Ниже приведена сессия
интерпретатора Питон (используя Питон 2.3 и новый модуль timeit,
который показывает насколько второй быстрей чем первый):
Вышеприведенные примеры немного натянуты, но общий принцип понятен.
Замечание: переместив импорт внутрь функции, можно увеличить начальную
скорость загрузки модуля, особенно если импортируемой модуль особо
не нужен. Это так называемая "отложенная" оптимизация -- избегать
работу (импортирование модуля, которое может быть затратным) до тех
пор, пока Вы точно не будете уверены, что Вам это необходимо.
Значительное преимущество будет только в том случае, если данный
модуль более нигде не будет импортироваться вообще (из любого другого
модуля) -- если модуль уже загружен (часто в случае стандартных
модулей, таких как string или re), то никакого выигрыша не будет.
Для того чтобы посмотреть, какие модули загружены --
см. в sys.modules.
Хороший пример отложенного импорта:
email = None
def parse_email():
global email
if email is None:
import email
...
В данном случае, модуль email будет импортирован один раз, при первом
запуске parse_email().
Агрегация данных
Накладные расходы на вызов функции в Питоне достаточно велики,
особенно по сравнению со встроенными функциями. Поэтому крайне
рекомендуется чтобы там, где это возможно, функции обрабатывали
агрегаторы данных. Ниже приведен пример (немного искуcственный):
import time
x = 0
def doit1(i):
global x
x = x + i
list = range(100000)
t = time.time()
for i in list:
doit1(i)
print "%.3f" % (time.time()-t)
и
import time
x = 0
def doit2(list):
global x
for i in list:
x = x + i
list = range(100000)
t = time.time()
doit2(list)
print "%.3f" % (time.time()-t)
Ниже -- пример интерактивной сессии:
>>> t = time.time()
>>> for i in list:
... doit1(i)
...
>>> print "%.3f" % (time.time()-t)
0.758
>>> t = time.time()
>>> doit2(list)
>>> print "%.3f" % (time.time()-t)
0.204
Даже написанный на Питоне, второй пример примерно в четыре раза
быстрее чем первый. Будь doit написан на C, разница была бы еще более
заметна (замена Питон-цикла for на C-цикл, плюс удаление большого
количества вызовов функций).
Делать проверки асинхронных событий реже
Питон-интерпретатор выполняет некоторые периодические проверки.
В частности, он решает, запускать или не запускать очередной поток
(thread); запускать ожидающий (pending) вызов или не запускать (обычно
это вызов обработчика сигналов). В большинстве случаев, такие проверки
ничего не делают, так что их периодическое выполнение может понизить
производительность. Эти функции находятся в модуле sys;
setcheckinterval указывает как часто интерпретатор должен выполнять
эти периодические проверки. Для Питона версий до 2.3 по умолчанию
это 10. В 2.3 его повысили до 100. Если Вы не работаете с потоками
и Вы не думаете, что будете обрабатывать много сигналов, увеличение
значения этой переменной может увеличить производительность, иногда
существенно.
Python - не C
Он также не Perl, не Java, не C++ и не Haskell. Будьте осторожны
в проецировании своих знаний с других языков на Питон. Простой пример
демонстрирует это:
% timeit.py -s 'x = 47' 'x * 2'
1000000 loops, best of 3: 0.574 usec per loop
% timeit.py -s 'x = 47' 'x << 1'
1000000 loops, best of 3: 0.524 usec per loop
% timeit.py -s 'x = 47' 'x + x'
1000000 loops, best of 3: 0.382 usec per loop
Теперь аналогичный код на C (показана версия только со сложением):
#include <stdio.h>
int
main (int argc, char **argv) {
int i = 47;
int loop;
for (loop=0; loop<500000000; loop++)
i + i;
}
и время выполнения:
% for prog in mult add shift ; do
< for i in 1 2 3 ; do
< echo -n "$prog: "
< /usr/bin/time ./$prog
< done
< echo
< done
mult: 6.12 real 5.64 user 0.01 sys
mult: 6.08 real 5.50 user 0.04 sys
mult: 6.10 real 5.45 user 0.03 sys
add: 6.07 real 5.54 user 0.00 sys
add: 6.08 real 5.60 user 0.00 sys
add: 6.07 real 5.58 user 0.01 sys
shift: 6.09 real 5.55 user 0.01 sys
shift: 6.10 real 5.62 user 0.01 sys
shift: 6.06 real 5.50 user 0.01 sys
Заметьте, что в Питоне существенным преимуществом обладает сложение
числа с самим собой, перед умножением на два или перед сдвигом на один
бит влево. На C и на всех современных архитектурах компьютеров, каждая
из этих трех арифметических операций кодируется в одну машинную
инструкцию, так что нет разницы (для C -- прим. j2a), какую
Вы выберите.
Частый "тест", который проделывают новички в Питоне, проецируя
Perl-идиому:
while (<>) {
print;
}
в Питон код, так что это выглядит примерно как:
import fileinput
for line in fileinput.input():
print line,
и приходят к заключению, что Питон гораздо медленнее Perl. Как уже
много раз говорилось, Питон медленнее Perl на одних задачах и быстрее
на других. Относительная производительность часто зависит от опыта
в каждом из двух языков.
Используйте xrange вместо range
В Питоне есть два способа получить диапазон чисел: range и xrange.
Большинство людей знают range, потомучто у него очевидное имя. xrange
в конце алфавита и поэтому его знают в горздо меньшей степени.
xrange -- генератор, почти идентичный следующему Питон-2.3 коду:
За исключением того, что он реализован на чистом C.
xrange имеет кое-какие ограничения. В частности, он работает только
с int, он не работает с long, float (они преобразовываются в int).
Между тем, он более бережно использует память, и если Вы отдельно
не храните yielded-объекты, то существует только один yielded-объект.
Разница вот в чем: когда Вы вызываете range, он создает список (list),
содержащий большое количество чисел (int, long или float). Весь список
создается сразу и все его элементы хранятся всё время. Это может быть
весьма расточительно, особенно когда количество чисел велико.
С другой стороны -- xrange -- не создает элементы сразу, он только
создает объект диапазона. Числа создаются тогда, когда Вы запрашиваете
данные у генератора, т.е. во время цикла по нему. Для примера:
xrange(sys.maxint) # Нет цикла, нет вызова .next, так что числа не выводятся
И по этой причине код сразу выполняется. Если Вы подставите range,
то Питон "зависнет" -- слишком времени займет генерация и размещение
в памяти sys.maxint чисел (на обычном PC это примерно 2.1 миллиарда).
В конце-концов он исчерпает всю память и выйдет.
В версиях Питона до 2.2, xrange объекты также поддерживали
оптимизации, такие как быстрое тестирование членства (i in xrange(n)).
Эта особенность была удалена из 2.2 ввиду неиспользования.
a = Test()
def example():
for i in xrange(0,100000):
a.check(i,"b","c")
import profile
profile.run("example()")
И Вы полагаете, что эта функция вызывается во многих местах по многу
раз. И если Вы видите, что выражение if замедляет скорость всегда
кроме первого раза, Вы можете сделать так:
a = Test2()
def example2():
for i in xrange(0,100000):
a.check(i,"b","c")
import profile
profile.run("example2()")
Итак, этот пример весьма натянут, однако, если выражение if весьма
сложно (или в нем много точек), Вы можете выиграть ситуацию, если
выполните его [выражение], будучи уверенным, что это выражение будет
истинно только в начале.
Профилирование кода
Первый шаг к увеличению скорости Вашей программы -- выяснение узких
мест. Не имеет смысла оптимизировать код, который вообще
не выполняется или выполняется и так быстро. Я ( Skip Montanaro --
прим. j2a) использую два модуля, которые помогают мне выделить
хотспоты в моем коде, profile и trace. В дальнейшем, я стал
использовать и timeit, который появился в Питоне версии 2.3
Модуль profile
Модуль profile включен в стандартную поставку Питона.
Его достаточно легко использовать для профилировки выполнения набора
функций. Полагая, что Ваша главная функция main вызывается
без аргументов и Вы хотите запустить ее под контролем модуля profile.
В самом простом случае достаточно:
import profile
profile.run('main()')
Когда main() завершит выполнение, модуль profile распечатает таблицу
вызовов функций и времена выполнения. Вывод может быть настроен
при помощи класса Stats также включенного в состав этого модуля.
В Питоне версии 2.4 profile позволяет профилировать встроенные функции
Питона и функции из модулей.
Модуль hotshot
Пакет hotshot, появившийся в Питоне 2.2, позиционируется
как замена модулю profile. Основной модуль написан на C, так что
использование hotshot должно меньше влиять на производительность и,
поэтому давать более аккуратные результаты по Вашей программе. Также
в поставку Питона (в папке Tools/scripts) входит hotshotmain.py,
который позволяет с командной строки запускать Вашу программу
под контролем hotshot.
Модуль trace
Модуль trace -- это развитие profile, которое я [42] Skip
Montanaro -- прим. j2a) написал изначально для выполнения некоторых
черновых тестов. С тех пор, как я написал его, он был серъезно
доработан другими людьми. Так что начиная с Питон 2.0 Вы можете найти
trace.py в папке Tools/scripts дистрибутива Питона. Начиная с Питона
весрии 2.3, он включен в стандартную библиотеку (папка Lib). Вы можете
скопировать его в папку с локальным bin, поставить право выполнения
и потом непосредственно запустить. Он позволяет выполнять трассировку
скриптов из командной строки:
% trace.py -t spam.py eggs
В Питоне 2.4 использовать его еще проще. Просто запустите python -m
trace.
Отдельной документации по нему нет, однако Вы можете выполнить pydoc
trace для просмотра встроенной документации.
Перевод http://gentooexperimental.org/~ferringb/bzr/pkgcore/HACKING
Не гарантируется, что приведенный код корректен ;)
Вся статистика для sempron 1.6ghz и python 2.4.2 (для платформы linux,
я так понимаю -- прим. j2a)
len
Будь внимателен насчет того, что в действительности делает
интерпретатор
Не использууй len(list_instance) когда тебе лишь нужно узнать, пустой
или не пустой список
l=[1]
if l: blah
# вместо
if len(l): blah
python смотрит вначале __nonzero__, потом __len__. Намного быстрее
будет, если ты явно укажешь, что смотреть.
$ python -m timeit -s 'l=[]' 'if len(l) > 0: pass'
1000000 loops, best of 3: 0.705 usec per loop
$ python -m timeit -s 'l=[]' 'if len(l): pass'
1000000 loops, best of 3: 0.689 usec per loop
$ python -m timeit -s 'l=[]' 'if l: pass'
1000000 loops, best of 3: 0.302 usec per loop
has_key
Не используй has_key, лучше положись на оператор 'in'.
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' 'd.has_key(1999999)'
1000000 loops, best of 3: 0.512 usec per loop
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' '1999999 in d'
1000000 loops, best of 3: 0.279 usec per loop
Python интерпретирует 'in' используя __contains__ объекта. Делает
он это быстрей, выполняя getattr, чем текущий python-код. Например,
код приведенный выше, использует d.__contains__; если ты сделаешь
d.has_key или d.__contains__, то по скорости это будет одинаково.
Используя 'in', интерпретатор делает поиск (lookup), который быстрее.
Так что... будь внимателен, как интерпретатор выполняет твой код.
Доступ к атрибутам из python-кода медленнее, чем <<изнутри>>
интерпретатора.
Если сомневаешься -- python -m timeit спасет тебя ;)
[], {} как значения по умолчанию
Не используй [] или {} как значения по умолчанию в определениях
функций/процедур.
Значения по умолчанию инициируются, когда определяются
функции/классы/методы, не в момент вызова.
Конечный результат, если использует изменяемое значение по умолчанию,
должен использовать None и проверять это;
это можно не делать если ты точно знаешь, что это значение
не меняется.
Карри-функции
Видимые карри-функции (см. напр. И опять о функциональном
программировании на Python -- прим. j2a) должны быть документированы.
Когда ты используешь методы карринга (pkgcore.util.currying))
для изменений функций, документируй их при помощи pretty_docs.
Если этого не делать, то вывод pydoc для такого объекта не будет особо
полезным.
Юнит-тесты
Весь код должен иметь тесты. Мы используем twisted.trial; по идее
работает на python >= 2.2 (на python <2.2 часты ложные срабатывания
в тестах spawn). Регрессия должны обязательно иметь тесты,
исключая таким образом дурацкие ошибки (опечатки, например).
Еще допустимо, когда на готовый код не хватает тестов, но при
объединении/интегрировании тесты обязательны.
Единственное, что (на данный момент) не требует тестов --
это интерактивность ebuild; тестирование интерфейса достаточно сложно,
хотя по идее, должно быть реализовано.
Если тесты отсутствуют (например, автор не написал их изначально),
крайне желательно создание новых тестов :)
Код, работающий с ФС
Если дело касается кода, работающего с ФС, то обычно лучше сделать
try, чем сначала спросить, а потом сделать try... но ты должен
проверить это ;)
...существующий файл (но пустой, чтобы исключить влияние потерь
при чтении)...
$ echo > dar
$ python -m 'timeit' -s 'import os' 'os.path.exists(<<dar>>) and open(<<dar>>).read()'
10000 loops, best of 3: 36.4 usec per loop
$ python -m 'timeit' -s 'import os' $'trypen(<<dar>>).read()nexcept IOError: pass'
10000 loops, best of 3: 22 usec per loop
...несуществующий файл...
$ rm foo
$ python -m 'timeit' -s 'import os' 'os.path.exists(<<foo>>) and open(<<foo>>).read()'
10000 loops, best of 3: 29.8 usec per loop
$ python -m 'timeit' -s 'import os' $'trypen(<<foo>>).read()nexcept IOError: pass'
10000 loops, best of 3: 27.7 usec per loop
Есть разница, не правда ли?
Заметь, что это касается только кода, работающего с ФС; системные
вызовы не быстры -- если это не переключаемые системные вызовы, смотри
следющий раздел.
Исключения
Отлов исключений в python-коде (в отличии от CPython) -- медленны.
статистика для python-2.4.2:
Возбуждение исключения:
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' $'try: d[1999]nexcept KeyError: pass'
100000 loops, best of 3: 8.7 usec per loop
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' $'1999 in d and d[1999]'
1000000 loops, best of 3: 0.492 usec per loop
В случае, если исключение не возбуждается, потеря ресурсов
на try/except:
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' $'try: d[0]nexcept KeyError: pass'
1000000 loops, best of 3: 0.532 usec per loop
$ python -m 'timeit' -s 'd=dict(zip(range(1000), range(1000)))' $'d[0]'
1000000 loops, best of 3: 0.407 usec per loop
Не советую писать код, которые не защищает сам себя, но лишь будь
внимателен насчет того, что делает твой код; и обращай внимание,
что исключения в python-коде весьма накладны, в силу вызываемой
ими "машинерии".
setdefault для словаря
Еще один пример насчет использования или неиспользования setdefault
и get методов словаря:
Ключ существует:
Через возбуждение исключения:
$ python -m timeit -s 'd=dict.fromkeys(range(100))' 'try: x=d' 'except KeyError: x=42'
1000000 loops, best of 3: 0.548 usec per loop
d.get:
$ python -m timeit -s 'd=dict.fromkeys(range(100))' 'x=d.get(1, 42)'
1000000 loops, best of 3: 1.01 usec per loop
Ключ не существует:
Через исключение:
$ python -m timeit -s 'd=dict.fromkeys(range(100))' 'try: x=d[101]' 'except KeyError: x=42'
100000 loops, best of 3: 8.8 usec per loop
d.get:
$ python -m timeit -s 'd=dict.fromkeys(range(100))' 'x=d.get(101, 42)'
1000000 loops, best of 3: 1.05 usec per loop
Если кратко, то: Если ты знаешь, что ключ присутствует, get медленнее.
Если не знаешь -- get в помощь.
Другими словами, используй его вместо теста на существование ключа
во время доступа.
Конечно, это только касается случая, когда значение по умолчанию
простое. Если же оно содержит нечто, более "тяжелое" чем except,
то try/except лучше, поскольку в этом случае, значение
не конструируется для каждого элемента заново. Так что, если
сомневаешься и код критичный по скорости -- сделай замеры скорости
с timeit, вместо того чтобы понимать как аксиому "исключения
медленны" или "[] быстры"
Утечки переменных
Для некоторых конструкций, в CPython существуют <<утечки>> переменных
в локальное пространство имен.
def f(s):
while True:
try:
some_func_that_throws_exception()
except Exception, e:
# e exists in this namespace now.
pass
# some other code here...
Для кода выше, e "протекает" в пространство имен f --
это неиспользуемая память, на которую ссылаются и она будет
сохраняться, пока цикл не закончится.
Python "пропускает" переменные в локальное пространство имен -- будь
внимателен с этим и явно удаляй неиспользуемые ссылки, когда имеешь
дело с большими объектами, особенно с исключениями.
class c:
d = {}
for x in range(1000):
d[x] = x
Вышеприведенный класс надуман, но ситуация в том, что c.x теперь
существует --
x из цикла "протекает" в пространство имен класса и остается там.
Не оставляй неиспользуемые переменные в пространстве имен класса.
Переменные, которые "протекают" из циклов for обычно не существенны,
лишь имей их ввиду -- особенно, если переменная ссылается на большой
объект (это сохранит память).
Итак... циклы for "текут", списочные выражения "текут",
вспомогательные переменные в try/except тоже "текут".
(x)range
Используй xrange всегда, за исключением ситуации, когда тебе нужно
сгенерировать и сохранить результат range.
$ python -m timeit 'for x in range(10000): pass'
100 loops, best of 3: 2.01 msec per loop
$ python -m timeit 'for x in xrange(10000): pass'
1000 loops, best of 3: 1.69 msec per loop
Удаление из списка
Удаление элементов из списка -- достаточно медленная операция,
особенно если бОльшая часть списка остается. Если тебе действительно
нужно оставлять бОльшую часть списка при удалении -- модуль deque (на
самом деле, пакет collections, появился в python-2.4 -- прим. j2a)
тебе поможет.
$ python -m timeit $'l=range(1000);i=0;nwhile i < len(l):ntif l[i]!="asdf":del l[i]ntelse:i+=1'
100 loops, best of 3: 4.12 msec per loop
$ python -m timeit $'l=range(1000);nfor i in xrange(len(l)-1,-1,-1):ntif l[i]!="asdf":del l[i]'
100 loops, best of 3: 3 msec per loop
$ python -m timeit 'l=range(1000);l=[x for x in l if x == "asdf"]'
1000 loops, best of 3: 1 msec per loop
Так что, удалять элементы из списка -- самый медленный способ. Самый
быстрый способ -- списковые выражения. Т.о., если есть возможность
не удалять элементы -- не удаляй их.
None
Если ты проверяешь на None, обрати внимание на оператор 'is'. Он не
использует протокол равенства и сравнивает непосредственно указатели.
$ python -m timeit '1 != None'
1000000 loops, best of 3: 0.721 usec per loop
$ python -m timeit '1 is not None'
1000000 loops, best of 3: 0.343 usec per loop
Устаревшие модули
Не используй types, используй isinstance (не по причинам оптимизации,
types -- само по себе плохо)
Не используй strings. Есть исключения, но лучше используй методы.
Не используй модуль stats модуль только для того, чтобы получить
аттрибут stat, например:
import stats
l=os.stat("asdf")[stat.ST_MODE]
# также может быть сделано при помощи: (и это более "правильно")
l=os.stat("asdf").st_mode
Обработка исключений
Знай возбуждаемые исключение и перехватывай только нужные из них.
try:
blah
except Exception:
blah2
Вышеприведенный код ловит все исключения, в том числе
и KeyboardInterrupt, и SystemExit. Это означает, что <<кривая>>
обработка исключений приводит к тому, что программа не правильно
обрабатывает ctrl+c.
Так что перехватывай только нужные тебе исключения.
Кортежи против списков
Первый неизменяемый, второй -- изменяемый.
Списки занимают больше памяти чем потом используют реально (в
большинстве случаев это хорошо).
Если ты генерируешь большое количество последовательностей, которые
не будут изменяться, используй кортежи.
Неизменяемые объекты
Копирование неизменяемых (например, кортежи, строки) объектов -- тупо.
Например:
copy.copy1,2,3 -- это тупо; никто не сделает такую явную ошибку, но в
большом коде народ их делает (к примеру, пытается скопировать строку,
используя [:], результатом будет сама строка, поскольку
она не изменяема).
Их нельзя поменять, так что и нечего пытаться их скопировать.
__del__
Метод __del__ имеет один досадный побочный эффект: он блокирует сборку
мусора, когда объект вызывается в цикле -- интерпретатор не знает,
что __del__ удалит ссылку на объект, так что он не в курсе (в общем
случае) как остановить цикл.
Так что... если используешь метод __del__, убедись что объект
не зацикливается (не важно, аккуратно ли используются данные,
или используется weakref)
Узкое место
Обычно не python медленный, а твой алгоритм.
l = []
for x in data_generator():
if x not in l:
l.append(x)
Для этого кода в лучшем случае O(1) (например, когда генератор выдает
одни нули). Худший случай -- O(N^2).
l=set()
for x in data_generator():
if x not in l:
l.add(x)
Лучший/худший случай теперь константа (в действительности, не совсем,
из-за потенциального расширения множества, но в данном случае
это можно опустить).
Далее, первый цикл в действительности вызывает __eq__ для сравнения
x для каждого элемента, так что здесь возможна ощутимая потеря
производительности в случае сложного объекта.
А второй цикл вызывает __hash__ для каждого x один раз. Чувствуешь
разницу?
Технически, второй цикл всё еще остается неэффективным:
l=set(data_generator())
проще и быстрее.
И реальные цифры для народа, который не видит, насколько всё плохо:
$ python -m timeit $'l=[]nfor x in xrange(1000):ntif x not in l:l.append(x)'
10 loops, best of 3: 74.4 msec per loop
$ python -m timeit $'l=set()nfor x in xrange(1000):ntif x not in l:l.add(x)'
1000 loops, best of 3: 1.24 msec per loop
$ python -m timeit 'l=set(xrange(1000))'
1000 loops, best of 3: 278 usec per loop
Небольшая разница, да?
Это не означает, что множества (set) автоматически лучше везде,
это не так: например, для одной операции поиска в диапазоне, косвенные
затраты (оверхед) гораздо больше, чем для линейного поиска.
$ python -m timeit -s 'l=range(50)' $'if 1001 in set(l): pass'
100000 loops, best of 3: 12.2 usec per loop
$ python -m timeit -s 'l=range(50)' $'if 1001 in l: pass'
10000 loops, best of 3: 7.68 usec per loop