Повышение производительности Python приложений


Russian translation of the Python Speed - Performance tips article, translated by Anton Vlasenko .
Перевод статьи Python Speed - Performance tips
Автор перевода: Власенко А.П., магистр ДонНТУ

×

Внимание!

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

Эта статья посвящена инструкциям по повышению производительности ваших приложений на Python.


Python существенно изменился с тех пор, как я опубликовал свою первую версию статьи "Быстрый Python" в 1996, а значит некоторые правила могли потерять актуальность. Поэтому я перенес статью в Python wiki, в надежде, что другие помогут мне содержать ее в актуальном состоянии. Всегда проверяйте представленные здесь советы на вашем приложении и на конкретном интерпретаторе Python, который вы собираетесь использовать. Не стоит следовать этим советам вслепую, без проверки, соглашаясь, что один метод быстрее другого. В секции "профилирование" говорится об этом подробно. С тех пор, как была написана оригинальная статья, появились также следующие инструменты: Cython, Pyrex, Psyco, Weave, PyInline, PyPy, которые могут существенно повысить производительность вашего приложения, облегчая процесс преобразования критичного для производительности кода в C или в машинный код.

Предисловие: оптимизируйте то, что требует оптимизации

Не оптимизируйте программу, если производительность является достаточной.



Чтобы узнать, что делает вашу программу медленной, нужно сначала дать приложению выполниться, выдав правильный результат. Если вы решите, что программа выполняется слишком медленно, вы должны профилировать программу, чтобы определить самую медленную её часть. Позаботьтесь о быстром автоматизированном тесте, который должен подтверждать, что будущие оптимизации не сломают вашу программу и не повлияют на правильность ее работы.

Вкратце:
  1. Сделайте приложение.
  2. Проверьте правильность его работы.
  3. Профилируйте, если производительность окажется недостаточной.
  4. Оптимизируйте.
  5. Повторите шаг 2.

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

Сортировка

Сортировка списков базовых объектов в Python реализована эффективно. Метод sort() для сортировки списков принимает в качестве дополнительного аргумента функцию, которая может быть использована как признак, используемый во время сортировки. Это достаточно очевидно, но это также можно существенно замедлить ваши сортировки, т. к. эта функция будет вызвана несколько раз. В Python 2.4 рекомендуется использовать аргумент key для встроенной сортировки, который должен быть быстрейшим способом сортировки. Следующие руководство Гвидо ван Россума следует использовать только для более ранних версий Python (до 2.4):

Альтернативный способ ускорить сортировку — создание списка кортежей, где первый элемент — ключ для сортировки, который правильным образом отсортируется используя стандартную сортировку. Второй элемент кортежа — оригинальный список. Это так называемый Schwartzian Transform, также известный как DecorateSortUndecorate(DSU, идиома языка Perl).

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

      def sortby(somelist, n):
      	nlist = [(x[n], x) for x in somelist]
      	nlist.sort()
      	return [val for (key, val) in nlist]

Перезапись списка сортированным также может быть достигнута, повторяя функционал встроенной функции сортировки:

      def sortby_inplace(somelist, n):
      	somelist[:] = [(x[n], x) for x in somelist]
      	somelist.sort()
      	somelist[:] = [val for (key, val) in somelist]
      	return somelist
Пример использования:
      >>> somelist = [(1, 2, 'def'), (2, -4, 'ghi'), (3, 6, 'abc')]
      >>> somelist.sort()
      >>> somelist
      [(1, 2, 'def'), (2, -4, 'ghi'), (3, 6, 'abc')]
      >>> nlist = sortby(somelist, 2)
      >>> sortby_inplace(somelist, 2)
      >>> nlist == somelist
      True
      >>> nlist = sortby(somelist, 1)
      >>> sortby_inplace(somelist, 1)
      >>> nlist == somelist
      True
В Python 2.4 добавлен необязательный параметр key, который существенно упрощает нестандартную сортировку:
      n = 1
      import operator
      nlist.sort(key=operator.itemgetter(n))
      # Используйте sorted() если вы не хотите
      # перезаписывать существующий список сортированным
      # sortedlist = sorted(nlist, key=operator.itemgetter(n))      

Обратите внимание, что оригинальный список не используется для сортировки, только лишь возвращаемый ключ. Это эквивалентно следующей конструкции:

      n = 1
      nlist = [(x[n], i, x) for (i, x) in enumerate(nlist)]
      nlist.sort()
      nlist = [val for (key, index, val) in nlist]

Конкатенация строк

Точность и актуальность данной секции подвержена сомнению из-за последних реализаций Python. В CPython 2.5 конкатенация строк реализована очень быстро, однако это может не относиться к другим реализациям.

В Python строки неизменяемые. Этот факт часто вызывает затруднения у новичков. Неизменность строк это одновременно некоторое количество достоинств и недостатков. К достоинствам можно отнести то, что строки могут быть использованы как значения в словарях и отдельные копии строк могут быть связаны с несколькими переменными-ссылками (Python автоматически связывает строки из 1-2 символов). К минусам можно отнести то, что вы не можете просто выполнить запрос “измени все символы 'a' в строке на символы 'б'”. Вместо этого, вы должны создать новую строку с желаемыми свойствами и изменениями. Это постоянное копирование может привести к существенным задержкам в Python приложениях.

Избегайте следующего:

# Так лучше не делать!
      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) # Рекомендованный способ       
От переводчика:
>>> timeit('s = "".join(l)', 's=""; l = ["foo", "bar", "spam"]')
0.3600640296936035
>>> timeit('for sub in l: s+=sub', 's=""; l = ["foo", "bar", "spam"]')
0.857280969619751

Избегайте:

out = "<html>" + head + prologue + query + tail + "</html>" # Так лучше не делать!

Вместо этого используйте:

out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail) # Рекомендованный способ.

Для улучшения читабельности (никак не влияет на производительность программы, только на вашу, как программиста), используйте словарное описание:

out = "%(head)s%(prologue)s%(query)s%(tail)s" % locals() # Рекомендованный для читабельности способ.

Последние два способа должны быть намного быстрее, особенно если через них пропускается огромное количество CGI-запусков, а также более легкими в сопровождении. К тому же, не рекомендованный медленный способ стал еще медленнее в Python 2.0 после добавления возможности перегрузить операторы сравнения. Теперь Python тратит больше времени, чтобы определить как складывать две строки (не забываем, что Python производит поиск методов прямо во время работы программы).

Примечание переводчика:

Для того, чтобы можно было сделать какие-либо выводы, привожу следующий бенчмарк (Python 3.2.3):

>>> timeit('"<html>" + foo + bar + "</html>"', 'foo = "Foo"; bar = "Bar"')
0.44135117530822754
>>> timeit('"<html>%s%s</html>" % (foo, bar)', 'foo = "Foo"; bar = "Bar"')
0.8441739082336426

>>> timeit('out = "<html>" + foo + "<br>" + bar + "</html>"', 'foo = "Foo"; bar = "Bar"')
0.5593209266662598
>>> timeit('out = "<html>%s<br>%s</html>" % (foo, bar)', 'foo = "Foo"; bar = "Bar"')
0.8703830242156982

>>> timeit('out = "<html>" + foo + "<br>" + bar + "<br>" + foo + "<br>" + bar + "</html>"', 'foo = "Foo"; bar = "Bar"')
1.172637939453125
>>> timeit('out = "<html>%s<br>%s<br>%s<br>%s</html>" % (foo, bar, foo, bar)', 'foo = "Foo"; bar = "Bar"')
1.0721790790557861   

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

Циклы

Python поддерживает несколько циклических конструкций. Оператор for является наиболее часто используемым. Он проходит циклом по каждому элементу выражения, присваивая каждому элементу переменную цикла. Если тело вашего цикла простое, процессорное время самой функции for может составлять существенную часть общей нагрузки на проведение операции. Именно здесь может пригодиться использование функции map. Функцию map можно представить как перенесение встроенной функции for на язык C. Единственное ограничение – телом цикла должен быть вызов функции. Еще один способ - списочные выражения , они выполняется быстрее, или, как минимум, так же быстро, как цикл map. Наглядный пример. Вместо того, чтобы проходить циклом по каждому элементу списка и переводить весь текст в верхний регистр:

newlist = []
for word in oldlist:
	newlist.append(word.upper())

Можно использовать map, чтобы перенести цикл на выполнение из интерпретатора в компилированный C код:

newlist = map(str.upper, oldlist)

Списочные выражения также были добавлены в Python 2.0. Они предоставляют более компактный и эффективный способ записи представленного выше примера:

newlist = [s.upper() for s in oldlist]

Генераторы были добавлены в Python начиная с версии 2.4. Они функционируют схожим со списочными выражениями или map образом, за исключением того, что они избегают генерации всего списка сразу. Вместо этого, объект-генератор предоставляет возможность выполнять операции пошагово, друг за другом, одновременно загружая в память лишь один объект внутри списка:

iterator = (s.upper() for s in oldlist)

Какой метод использовать зависит от используемой версии Python и манипулируемых данных. Гвидо ван Россум написал детальное руководство по оптимизации циклов, с которым обязательно стоит ознакомиться.

Избегание точек [ . ]

Допустим, вы не можете использовать функцию map или списочные выражения, вам остается использовать только for. Пример с циклом for имеет еще один недостаток. Такие операции, как newlist.append и word.upper, являются ссылками на функции, которые определяются каждый раз во время цикла. Оригинальный цикл можно заменить следующим:

upper = str.upper
newlist = []
append = newlist.append
for word in oldlist:
	append(upper(word))

Эта техника должна использоваться с предельной осторожность. Большие циклы становится труднее поддерживать, т. к. вам придется постоянно проверять код, чтобы понимать, что конкретно значат эти append и upper.

От переводчика:
Правила избежания точек относятся и к импорту модулей. В качестве примера:

>>> timeit.timeit ("math.sqrt(4)", 'import math')
0.3284459114074707
>>> timeit.timeit ('sqrt(4)', 'from math import sqrt')
0.25652194023132324

То есть лишь одно изменение правил импортирования приводит к 30% росту производительности одной итерации.

Локальные переменные

Последняя возможность ускорить цикл for — использование локальных переменных везде, где это возможно. Если наш пример перевести в функцию, append и upper становятся локальными переменными. Python работает гораздо эффективнее с локальными переменными, чем с глобальными.

def func():
	upper = str.upper
	newlist = []
	append = newlist.append
	for word in oldlist:
		append(upper(word))
	return newlist

На тот момент, когда я начал писать эту статью, я использовал 100MHz Pentium, на котором был запущен BSDI. Я получил следующие значения по времени исполнения при преобразовании списка слов /usr/share/dict/words (38,470 слов на то время) в верхний регистр:

Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54

Инициализация словарей

Представим, что вы создаете словарь для хранения количества встречающихся слов и вы уже разбили ваш текст на список слов. Вы можете использовать для этого нечто вроде:

wdict = {}
for word in words:
	if word not in wdict:
		wdict[word] = 0
	wdict[word] += 1

За исключением первого раза, каждый раз слово возвращает False проходя через оператор if. Когда вы считаете большое количество слов, скорее всего многие из них будут попадаться не один раз. В случаях, когда инициализация значения происходит лишь однажды, а основная работа идет над изменением значения, гораздо эффективнее будет использование оператора try:

wdict = {}
for word in words:
	try:
		wdict[word] += 1
	except KeyError:
		wdict[word] = 1

Важно перехватывать только ожидаемое исключение KeyError, чтобы избежать ситуации с перехватом исключения, которое вы на самом деле не сможете обработать. Третий способ становится доступным с выпуском Python 2. Словари теперь поддерживают метод get(), который установит значение по умолчанию, если необходимый ключ не был найден в словаре. Этот способ значительно сокращает цикл:

wdict = {}
get = wdict.get
for word in words:
	wdict[word] = get(word, 0) + 1

На данный момент все три варианта решения имеют схожую производительности (с разницей не более 10%), более или менее независимо от обрабатываемых данных. Также, если значение, которое хранится в словаре, является объектом или изменяемым списком, вы также можете использовать метод dict.setdefault:

      wdict.setdefault(key, []).append(new_element)      

Вы можете решить, что этот способ исключает производимое дважды обращение к ключу. На самом деле это не так (даже в Python 3), но, как минимум, двойное обращение производится средствами C. Еще одно решение — использование класса defaultdict:

from collections import defaultdict

wdict = defaultdict(int)

for word in words:
	wdict[word] += 1

Перегрузки от выражений импорта

Операция импорта может быть запущена где угодно в программном коде. Часто бывает полезным размещать их внутри функций, чтобы ограничить их видимость и уменьшить начальное время запуска приложения. Хотя интерпретатор Python оптимизирован, чтобы не производить импорт одного и того же модуля несколько раз, повторный вызов функции импорта может некоторым образом отразиться на производительности. Рассмотрим следующие фрагменты кода (взятые, как мне кажется, у Greg McFarlane):

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, даже не смотря на то, что doit2 обращается к модулю string глобально. Ниже представлена сессия интерпретатора Python 2.3 с модулем timeit, которая показывает, насколько второй способ быстрее первого:

>>> def doit1():
... 	import string
... 	string.lower('Python')
...
>>> import string
>>> def doit2():
... 	string.lower('Python')
...
>>> import timeit
>>> t = timeit.Timer(setup='from __main__ import doit1', stmt='doit1()')
>>> t.timeit()
11.479144930839539
>>> t = timeit.Timer(setup='from __main__ import doit2', stmt='doit2()')
>>> t.timeit()
4.6661689281463623

Строковые методы были представлены в Python 2.0. Они представляют версию, которая совсем обходится без импортирования и работает еще быстрее:

def doit3():
	'Python'.lower()

for num in range(100000):
	doit3()

Наглядное подтверждение:

>>> def doit3():
... 	'Python'.lower()
...
>>> t = timeit.Timer(setup='from __main__ import doit3', stmt='doit3()')
>>> t.timeit()
2.5606080293655396

Имейте в виду, импорт модуля внутри функции может ускорить начальную загрузку модуля, особенно если импортируемый модуль может не понадобиться. Это в общих терминах случай «ленивой» оптимизации — исключение лишней работы - импортирование тяжеловесного модуля только тогда, когда вы уверены, что он точно вам понадобится. Это является существенной оптимизацией только если модуль не будет загружен вовсе (из любого модуля) — если модуль уже был загружен (что является обычным случаем для многих стандартных модулей, таких как string или re), избегание импорта никак не оптимизирует ваш код. Чтобы узнать, какие модули загружены в систему, используйте sys.modules. Хороший способ выполнения «ленивого» импорта:

email = None

def parse_email():
	global email
	if email is None:
		import email
...

Таким способом модуль email будет импортирован лишь однажды, при первом вызове parse_email().

Агрегация данных

Затраты на вызов функции в Python относительно высоки, особенно сравнивая с запуском встроенных функций. Это позволяет сделать выводы, что функции, если это возможно, должны сами заниматься агрегацией данных. Рассмотрим пример.
Способ 1:

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)

Способ 2:

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

Даже написанный на Python, второй способ работает почти в четыре раза быстрее первого. Если бы функция doit была написана на C — разница была бы еще более существенной (заменяя цикл for, написанный на python, циклом for на C, а также с устранением большинства вызовов функций).

Уменьшение частоты выполнения операций

Интерпретатор Python проводит некоторые переодические проверки. В частности, он решает, стоит ли передавать управление другому потоку, или стоит передать управление отложенному вызову (обычно это вызов, установленный обработчиком сигнала). В большинстве случаев он простаивает без работы, и вызов этих проверок каждый цикл интерпретатора может замедлять работу приложения. Существует функция в модуле sys — setcheckinterval, в которой можно указать, через какой промежуток внутренних тактов интерпретатора следует выполнять переодические проверки. По отношению к релизу до 2.3 это значение установлено как 10. В 2.3 значение было увеличено до 100. Если вы не работаете с потоками и вы не расчитываете перехватывать большое количество сигналов, установка высокого значения этого параметра может повысить производительность, иногда значительно.

Python - не C++

А также не Perl, Java, C или Haskell. Будьте аккуратны, когда переносите свои знания из других языков для решения задач в Python. Обычный пример служит отличной демонстрацией:

% 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 

int main (int argc, char *argv[]) {
	int i = 47;
	int loop;
	for (loop=0; loop<500000000; loop++)
		i + i;
		return 0;
}

Сравним время запуска:

  % 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

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

Распространенный «тест», который часто проводят новые программисты на Python, переводя распространенную идиому языка Perl

while (<>) {
          print;
          }

В Python этот код выглядит примерно следующим образом:

import fileinput

for line in fileinput.input():
	print line,

Они используют этот пример чтобы сделать выводы, что Python, должно быть, намного медленнее Perl. Как уже неоднократно отмечали, Python медленнее Perl в некоторых задачах, и быстрее в других. Относительная производительность чаще зависит именно от вашего опыта в обоих языках.

Используйте xrange вместо range

Этот раздел не применим, если вы используете Python 3, где функция range предоставляет итератор над диапазоном заданной длины, и где функция xrange больше не существует. В ранних версиях (до Python 3) имеется два способа получить диапазон чисел: range и xrange. Большинство людей знает о range из-за его очевидного имени, в то время как xrange находится в конце алфавитного указателя и является менее известным.

Функция xrange является генератором, она в общих чертах эквивалентна следующему коду:

def xrange(start, stop=None, step=1):
	if stop is None:
		stop = start
		start = 0
	else:
		stop = int(stop)
	start = int(start)
	step = int(step)

	while start < stop:
		yield start
		start += step

За исключением того, что она реализована на чистом C.

Функция xrange имеет свои ограничения. Она работает только с целыми числами. Вы не можете использовать long или float (они будут переконвертированы в int, как показано выше). Тем не менее, она сохраняет много памяти, и если вы не сохраняете генерируемый объект где-либо, то только один объект существует в один момент времени. Разница заключается в следующем: когда вы вызываете функцию range, она создает список, который хранит в себе огромное количество числовых объектов (int, float, long). Все эти объекты создаются одновременно, и все они существуют вместе в один момент времени. Это может быть проблемой, если массив чисел велик. Напротив, xrange не создает числа одновременно — только собственный объект диапазона. Числовые объекты создаются только когда вы обращаетесь к генератору, то есть проходите по нему циклом. Например:

xrange(sys.maxint) # Нет цикла, нет запроса next, поэтому числа не были сгенерированы

Именно по этим причинам код выполняется моментально. Если вы запустите таким же образом range, Python заблокируется. Он будет слишком занят нахождением памяти для 9223372036854775807 (для 64-битной архитектуры) числовых объектов, чтобы делать что-либо еще. Скорее всего, процесс завершится исключением MemoryError. В версиях Python до 2.2 xrange поддерживал также быструю проверку на членство (i in xrange(n)), но эти функции были удалены из-за отсутствия применения.

Присваивание функции «на лету»

Скажем, у вас есть функция:

class Test:
	def check(self,a,b,c):
		if a == 0:
			self.str = b*100
		else:
			self.str = c*100

a = Test()
def example():
	for i in xrange(0,100000):
		a.check(i,"b","c")

import profile
profile.run("example()")

Скажем, эта функция вызывается откуда-то много раз.

Ваша проверка будет иметь блок проверок if, замедляющий выполнение каждый раз, кроме первого. Можно сделать так:

class Test2:
	def check(self,a,b,c):
		self.str = b*100
		self.check = self.check_post
	def check_post(self,a,b,c):
		self.str = c*100

a = Test2()
def example2():
	for i in xrange(0,100000):
		a.check(i,"b","c")

import profile
profile.run("example2()")

Этот пример немного странный, но если оператор if у вас содержит достаточно большое количество условий (или с множеством точек [.]), вы можете избежать его постоянного прохождения, особенно если вы знаете, что условие будет истинным только лишь в первый раз.

Профилирование кода

Первым шагом в оптимизации вашей программы является нахождение ресурсоемких мест. Трудно понять смысл оптимизации кода, который никогда не был запущен, или который уже работает быстро. Я использую два модуля для нахождения ресурсоемких точек моего кода, профилирования и трассировки. В поздних примерах я использую также модуль timeit, который появился в Python 2.3. Ознакомьтесь с отдельной статьей profiling для подробностей (на англ. языке).

Профилирование

Существует несколько профилирующих модулей, включенных в стандартные «батарейки» Python. Использование любого из них для профилирования запуска некоторого набора функций достаточно просто. Допустим, ваша главная функция называется main, не требует входящих аргументов и вы хотите запустить ее под контролем профилирующих модулей. В самом простом виде вы лишь запускаете:

import profile
profile.run('main()')           

Когда функция выполнится, профилирующий модуль распечатает таблицу вызовов функций и времени исполнения. Вывод может быть подкорректирован используя класс Stats, который включен в поставку модуля. Начиная с Python 2.4 профилирование позволяет исследовать также время выполнения встроенных модулей и модулей расширений. Более существенное описание профилирования, используя profile и pstats, может быть найдено по следующей ссылке (на англ.).

Модули cProfile и Hotshot

Начиная с Python 2.2, пакет hotshot представляется как замена для модуля profile, хотя использование cProfile сейчас является более предпочтительным. Основополагающий модуль написан на C, поэтому использование hotshot (или cProfile) должно приводить к гораздо меньшему влиянию на производительность и давать более точную картину того, как ваше приложение работает. Существует также программа hotshotmain.py, которая упрощает запуск вашего приложения под hotshot из командной строки.

Модуль trace

Модуль trace является побочным от profile модулем, который я написал чтобы проводить некоторые низкоуровневые тесты. Он был сильно модифицирован несколькими другими людьми с тех пор, как я выпустил мой изначальный код. В Python 2.0 вы можете найти trace.py в папке Tools/scripts вашего дистрибутива Python. Начиная с версии 2.3 он входит в стандартную библиотеку (папка Lib). Вы можете скопировать его в свою локальную директорию, выставить права на запуск и запустить напрямую. Так можно получить полную трассировочную инфромацию о вашем приложении:

% trace.py -t spam.py eggs

В Python 2.4 запустить его проще — используйте команду python -m trace. Для модуля не существует отдельной документации, но вы можете выполнить команду «pydoc trace» чтобы увидеть внутреннюю документацию.

Визуализация результатов профилирования

RunSnakeRun — GUI утилита от Mike Fletcher'a, которая визуализирует результаты cProfile используя квадратные карты. Вызовы функций и методов могут быть отсортированы по различным критериям, исходный код может отображаться рядом с визуализацией и статистикой вызовов. Пример использования:

runsnake some_profile_dump.prof

Gpof2Dot — утилита, базирующаяся на Python, которая может преобразовывать результаты профилирования в виде графиков, которые могут быть сохранены как PNG или SVG изображения. Типичная сессия профилирования в Python 2.5 выглядит следующим образом (в более старых версиях вам понадобиться использовать сам скрипт, вместо опции -m):

python -m cProfile -o stat.prof MYSCRIPY.PY [ARGS...]
python -m pbp.scripts.gprof2dot -f pstats -o stat.dot stat.prof
dot -ostat.png -Tpng stat.dot

PyCallGraph — модуль Python, который создает графики вызовов для Python приложений. Он генерирует PNG файл, отображающий вызовы функций и их связи с другими вызовами функций, количество вызовов функций и время их исполнения. Пример использования:

pycallgraph scriptname.py