справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Глава 11 . Тестирование, отладка, профилирование и оптимизация В отличие от программ, написанных на таких языках, как C или Java, программы на языке Python не обрабатываются компилятором, который создает выполняемую программу. В этих языках программирования ком- пилятор является первой линией обороны от программных ошибок – он отыскивает такие ошибки, как вызов функций с недопустимым количе- ством аргументов или присваивание недопустимых значений переменным (то есть выполняет проверку типов). В языке Python такие проверки вы- Python такие проверки вы- такие проверки вы- полняются только после запуска программы. Поэтому невозможно ска- зать, содержит программа ошибки или нет, пока она не будет запущена и протестирована. Но и это еще не все, – если не опробовать программу всеми возможными способами, когда поток управления пройдет все воз- можные ветви программы, всегда остается вероятность, что в программе скрыта какая-нибудь ошибка, которая ждет своего часа (к счастью, такие ошибки обычно обнаруживаются уже через несколько дней после переда- чи программы пользователю). В этой главе рассматриваются приемы и библиотечные модули, используе- мые для тестирования, отладки и профилирования программного кода на языке Python, способные помочь в решении подобных проблем. В конце гла- Python, способные помочь в решении подобных проблем. В конце гла- , способные помочь в решении подобных проблем. В конце гла- вы обсуждаются некоторые стратегии по оптимизации программного кода. Строки документирования и модуль doctest Если первая строка функции, класса или модуля является строкой, эта строка называется строкой документирования. Включение строк доку- ментирования считается хорошим тоном, потому что эти строки исполь- зуются в качестве источников информации различными инструментами разработки. Например, строки документирования можно просматривать с помощью команды help(), а также средствами интегрированной среды разработки на языке Python. Программисты обычно просматривают со- Python. Программисты обычно просматривают со- . Программисты обычно просматривают со- держимое строк документирования при проведении экспериментов в ин- Строки документирования и модуль doctest 237 терактивной оболочке, поэтому в эти строки принято включать короткие примеры сеансов работы с интерактивной оболочкой. Например: # splitter.py def split(line, types=None, delimiter=None): “””Разбивает текстовую строку и при необходимости выполняет преобразование типов. Например: ёё >>> split(‘GOOG 100 490.50’) [‘GOOG’, ‘100’, ‘490.50’] >>> split(‘GOOG 100 490.50’,[str, int, float]) [‘GOOG’, 100, 490.5] >>> ёё По умолчанию разбиение производится по пробельным символам, но имеется возможность указать другой символ-разделитель, в виде именованного аргумента: ёё >>> split(‘GOOG,100,490.50’,delimiter=’,’) [‘GOOG’, ‘100’, ‘490.50’] >>> “”” fields = line.split(delimiter) if types: fields = [ ty(val) for ty,val in zip(types,fields) ] return fields Типичная проблема, связанная с документацией, состоит в том, чтобы обе- спечить ее соответствие актуальной реализации функции. Например, про- граммист может изменить функцию и забыть обновить документацию. Помощь в решении этой проблемы может оказать модуль doctest. Модуль doctest собирает строки документирования, просматривает их на наличие примеров интерактивных сеансов и выполняет эти примеры, как последо- вательность тестов. Чтобы воспользоваться модулем doctest, обычно требу- ется создать отдельный модуль, который будет выполнять тестирование. Например, если предположить, что реализация функции из предыдущего примера находится в файле splitter.py, можно было бы создать файл test- splitter.py со следующим содержимым: # testsplitter.py import splitter import doctest ёё nfail, ntests = doctest.testmod(splitter) В этом фрагменте вызов doctest.testmod(module) запускает процесс тестиро- вания указанного модуля module и возвращает количество ошибок и общее количество выполненных тестов. Если все тесты прошли успешно, ничего не выводится. В противном случае на экране появится отчет об ошибках, в котором будут показаны различия между ожидаемыми и фактическими результатами. Если потребуется получить более подробный отчет о тести- ровании, можно вызвать функцию в виде testmod(module, verbose=True). 238 Глава 11. Тестирование, отладка, профилирование и оптимизация В противоположность созданию отдельного файла, выполнение тестов можно реализовать непосредственно в библиотечных модулях, для чего до- статочно включить в конец файла модуля следующий программный код: ... if __name__ == ‘__main__’: # тестирование самого себя import doctest doctest.testmod() После этого тестирование, основанное на содержимом строк документиро- вания, будет выполняться, если запустить модуль как самостоятельную программу. В противном случае, при импортировании файла, тесты будут игнорироваться. При тестировании функций модуль doctest ожидает вывод, в точности со- впадающий с тем, что будет получен в интерактивной оболочке. В резуль- тате такой вид тестирования очень чувствителен к лишним или отсутству- ющим пробельным символам и к точности представления чисел. В каче- стве примера рассмотрим следующую функцию: def half(x): “””Возвращает половину x. Например: ёё >>> half(6.8) 3.4 >>> “”” return x/2 Если провести тестирование этой функции с помощью модуля doctest, бу- дет получен следующий отчет об ошибках: ********************************************************************** File “half.py”, line 4, in __main__.half Failed example: half(6.8) Expected: 3.4 Got: 3.3999999999999999 ********************************************************************** (Перевод: ********************************************************************** Файл “half.py”, строка 4, в __main__.half Ошибочный пример: half(6.8) Ожидалось: 3.4 Получено: 3.3999999999999999 ********************************************************************** ) Модульное тестирование и модуль unittest 239 Чтобы исправить эту проблему, необходимо либо привести строку доку- ментирования в точное соответствие с получаемыми результатами, либо привести в документации более удачный пример. Модуль doctest чрезвычайно прост в использовании, поэтому не может быть никаких оправданий, чтобы не использовать его в своих программах. Однако имейте в виду, что модуль doctest – не тот инструмент, который можно использовать для полного тестирования программы. Применение этого модуля для полного тестирования может привести к чрезмерному раздуванию и усложнению строк документирования, что снижает полез- ность документации (пользователь наверняка будет недоволен, если он об- ратится за справочной информацией, а в ответ ему будет представлен спи- сок из 50 примеров, охватывающих все хитрости использования функции). Для такого тестирования предпочтительнее использовать модуль unittest. Наконец, модуль doctest имеет огромное количество параметров настрой- ки различных аспектов, определяющих, как выполнять тестирование и как отображать результаты. Поскольку в большинстве типичных случа- ев использования модуля эти параметры не требуют изменения, они здесь не рассматриваются. Дополнительную информацию по этой теме можно найти по адресу: http://docs.python.org/library/doctest.html. Модульное тестирование и модуль unittest Для более полноценного тестирования программ можно использовать мо- дуль unittest. При модульном тестировании разработчик пишет набор обособленных тестов для каждого компонента программы (например, для отдельных функций, методов, классов и модулей). Затем эти тесты ис- пользуются для проверки корректности поведения основных компонен- тов, составляющих крупные программы. По мере роста программы в раз- мерах модульные тесты для различных компонентов могут объединяться в крупные структуры и средства тестирования. Это может существенно упростить задачу проверки корректности поведения, а также определения и исправления проблем по мере их появления. Использование этого модуля иллюстрирует следующий фрагмент программного кода, взятый из преды- дущего раздела: # splitter.py def split(line, types=None, delimiter=None): “””Разбивает текстовую строку и при необходимости выполняет преобразование типов. ... “”” fields = line.split(delimiter) if types: fields = [ ty(val) for ty,val in zip(types,fields) ] return fields Если потребуется написать модульные тесты для проверки различных аспектов применения функции split(), можно создать отдельный модуль testsplitter.py , например: 240 Глава 11. Тестирование, отладка, профилирование и оптимизация # testsplitter.py import splitter import unittest ёё # Модульные тесты class TestSplitFunction(unittest.TestCase): def setUp(self): # Выполнить настройку тестов (если необходимо) pass def tearDown(self): # Выполнить завершающие действия (если необходимо) pass def testsimplestring(self): r = splitter.split(‘GOOG 100 490.50’) self.assertEqual(r,[‘GOOG’,’100’,’490.50’]) def testtypeconvert(self): r = splitter.split(‘GOOG 100 490.50’,[str, int, float]) self.assertEqual(r,[‘GOOG’, 100, 490.5]) def testdelimiter(self): r = splitter.split(‘GOOG,100,490.50’,delimiter=’,’) self.assertEqual(r,[‘GOOG’,’100’,’490.50’]) ёё # Запустить тестирование if __name__ == ‘__main__’: unittest.main() Чтобы запустить тестирование, достаточно просто запустить интерпрета- тор Python, передав ему файл testsplitter.py. Например: % python testsplitter.py ... ---------------------------------------------------------------------- Run 3 tests in 0.014s ёё OK В своей работе модуль unittest опирается на объявление класса, производ- ного от класса unittest.TestCase. Отдельные тесты определяются как мето- ды, имена которых начинаются со слова ‘test’, например ‘testsimplestring’, ‘testtypeconvert’ и так далее. (Важно отметить, что имена методов могут выбираться произвольно, главное, чтобы они начинались со слова ‘test’.) Внутри каждого теста выполняются проверки различных условий. Экземпляр t класса unittest.TestCase имеет следующие методы, которые могут использоваться для тестирования и управления процессом тестиро- вания: t.setUp() Вызывается для выполнения настроек, перед вызовом любых методов те- стирования. t.tearDown() Вызывается для выполнения заключительных действий после выполне- ния всех тестов. Модульное тестирование и модуль unittest 241 t.assert_(expr [, msg]) t.failUnless(expr [, msg]) Сообщает об ошибке тестирования, если выражение expr оценивается как False . msg – это строка сообщения, объясняющая причины ошибки (если за- дана). t.assertEqual(x, y [,msg]) t.failUnlessEqual(x, y [, msg]) Сообщает об ошибке тестирования, если x и y не равны. msg – это строка со- общения, объясняющая причины ошибки (если задана). t.assertNotEqual(x, y [, msg]) t.failIfEqual(x, y, [, msg]) Сообщает об ошибке тестирования, если x и y равны. msg – это строка со- общения, объясняющая причины ошибки (если задана). t.assertAlmostEqual(x, y [, places [, msg]]) t.failUnlessAlmostEqual(x, y, [, places [, msg]]) Сообщает об ошибке тестирования, если числа x и y не совпадают с точно- стью до знака places после десятичной точки. Проверка выполняется за счет вычисления разности между x и y и округления результата до указанного числа знаков places после десятичной точки. Если результат равен нулю, числа x и y можно считать почти равными. msg – это строка сообщения, объ- ясняющая причины ошибки (если задана). t.assertNotAlmostEqual(x, y, [, places [, msg]]) t.failIfAlmostEqual(x, y [, places [, msg]]) Сообщает об ошибке тестирования, если числа x и y совпадают с точностью до знака places после десятичной точки. msg – это строка сообщения, объ- ясняющая причины ошибки (если задана). t.assertRaises(exc, callable, ...) t.failUnlessRaises(exc, callable, ...) Сообщает об ошибке тестирования, если вызываемый объект callable не возбуждает исключение exc. Остальные аргументы методов передаются вызываемому объекту callable, как аргументы. Для тестирования набора исключений в аргументе exc передается кортеж с этими исключениями. t.failIf(expr [, msg]) Сообщает об ошибке тестирования, если выражение expr оценивается как True . msg – это строка сообщения, объясняющая причины ошибки (если за- дана). t.fail([msg]) Сообщает об ошибке тестирования. msg – это строка сообщения, объясняю- щая причины ошибки (если задана). t.failureException В этом атрибуте сохраняется последнее исключение, перехваченное в те- сте. Может использоваться, когда необходимо не только проверить, что ис- 242 Глава 11. Тестирование, отладка, профилирование и оптимизация ключение возбуждается, но что при этом оно сопровождается требуемым значением, – например, когда необходимо проверить сообщение, генери- руемое исключением. Следует отметить, что модуль unittest имеет огромное количество дополни- тельных параметров настройки, используемых для группировки тестов, создания наборов тестов и управления окружением, в котором выполняют- ся тесты. Эти особенности не имеют прямого отношения к процессу созда- ния тестов (классы обычно пишутся независимо от того, как в действитель- ности выполняются тесты). В документации, по адресу http://docs.python. org/library/unittest.html , можно найти дополнительную информацию о том, как организовать тесты для крупных программ. Отладчик Python и модуль pdb В состав Python входит простой отладчик командной строки, который реа- Python входит простой отладчик командной строки, который реа- входит простой отладчик командной строки, который реа- лизован в виде модуля pdb. Модуль pdb поддерживает возможность поста- варийной отладки, исследования кадров стека, установки точек останова, выполнения исходного программного кода в пошаговом режиме и вычис- ления выражений. Существует несколько функций для вызова отладчика из программы или из интерактивной оболочки Python. run(statement [, globals [, locals]]) Выполняет строку statement под управлением отладчика. Приглашение к вводу отладчика появляется непосредственно перед выполнением какого- либо программного кода. Ввод команды ‘continue’ инициирует выполнение этого кода. Аргументы globals и locals определяют глобальное и локаль- ное пространство имен соответственно, в котором будет выполняться про- граммный код. runeval(expression [, globals [, locals]]) Вычисляет выражение в строке expression под управлением отладчика. Приглашение к вводу отладчика появляется непосредственно перед вы- полнением какого-либо программного кода, поэтому чтобы вычислить зна- чение выражения, необходимо ввести команду ‘continue’, которая запустит функцию run(). В случае успеха возвращается значение выражения. runcall(function [, argument, ...]) Вызовет функцию function под управлением отладчика. Аргумент function должен быть вызываемым объектом. Дополнительные аргументы переда- ются функции function. Приглашение к вводу отладчика появляется непо- средственно перед выполнением какого-либо программного кода. По завер- шении возвращается значение функции function. set_trace() Запускает отладчик в точке вызова этой функции. Может использоваться для создания точек останова в интересующих местах программы. Отладчик Python и модуль pdb 243 post_mortem(traceback) Запускает поставарийную отладку с использованием объекта traceback, со- держащего трассировочную информацию. Объект traceback обычно можно получить с помощью такой функции, как sys.exc_info(). pm() Переходит в режим поставарийной отладки с использованием объекта traceback , сгенерированного последним исключением. Из всех функций, вызывающих отладчик, самой простой в использова- нии, пожалуй, является функция set_trace(). Когда при работе со слож- ным приложением выявляется какая-либо проблема, можно просто вста- вить вызов set_trace() в программный код и запустить приложение. Когда интерпретатор встретит этот вызов, выполнение программы будет при- остановлено и управление будет передано отладчику, с помощью которо- го можно будет исследовать окружение, в котором протекает выполнение программы. Выполнение программы будет продолжено сразу же после вы- хода из отладчика. Команды отладчика После запуска отладчика выводится приглашение к вводу (Pdb), как пока- зано ниже: >>> import pdb >>> import buggymodule >>> pdb.run(‘buggymodule.start()’) > (Pdb) (Pdb) – это приглашение к вводу отладчика, в котором распознаются сле- дующие команды. Обратите внимание, что некоторые команды имеют две формы – краткую и длинную. Для обозначения обеих форм в описании ко- манд использованы круглые скобки. Например, h(elp) означает, что можно ввести команду h или help. [!]statement Выполняет инструкцию statement (однострочную) в контексте текущего ка- дра стека. Символ восклицательного знака можно опустить, но его исполь- зование обязательно, если первое слово инструкции statement совпадает с командой отладчика. Чтобы определить глобальную переменную, можно предварить инструкцию присваивания командой «global» в той же строке: (Pdb) global list_options; list_options = [‘-l’] (Pdb) a(rgs) Выводит список аргументов текущей функции. alias [name [command]] Создает псевдоним name для команды command. Подстроки, такие как ‘%1’,’%2’ и так далее, в строке command замещаются значениями параметров, которые 244 Глава 11. Тестирование, отладка, профилирование и оптимизация указываются при вводе псевдонима. Подстрока ‘%*’ соответствует всему списку параметров. Если значение command не задано, выводится текущее определение псевдонима. Псевдонимы допускается вкладывать друг в дру- псевдонима. Псевдонимы допускается вкладывать друг в дру- псевдонима. Псевдонимы допускается вкладывать друг в дру- . Псевдонимы допускается вкладывать друг в дру- Псевдонимы допускается вкладывать друг в дру- га и они могут содержать все, что допускается вводить в приглашении Pdb. Например: # Выводит переменные экземпляров (порядок использования: “pi classInst”) alias pi for k in %1._ _dict_ _.keys(): print “%1.”,k,”=”,%1._ _dict_ _[k] # Выводит переменные для экземпляра self alias ps pi self b(reak) [loc [, condition]] Устанавливает точку останова в местоположении loc. Значением loc может быть либо имя файла и номер строки, либо имя функции в модуле. Синтак- сис параметра имеет следующий вид: Значение Описание n Номер строки в текущем файле filename:n Номер строки в другом файле function Имя функции в текущем модуле module.function Имя функции в другом модуле Если параметр loc не задан, выводится список всех точек останова, уста- новленных на текущий момент. condition – это выражение, которое должно оцениваться как истина, чтобы произошел останов в данной точке. Каждой точке останова присваивается свой номер, который выводится по заверше- нии этой команды. Эти номера можно использовать в некоторых других командах отладчика, описываемых ниже. cl(ear) [bpnumber [bpnumber ...]] Сбрасывает точки останова с указанными номерами bpnumber. Если номера не указываются, команда сбросит все точки останова. commands [bpnumber] Определяет последовательность команд отладчика для автоматического выполнения по достижении точки останова bpnumber. Если необходимо ука- зать несколько команд, их можно ввести в нескольких строках и исполь- зовать слово end, как признак конца последовательности. Если включить в последовательность команду continue, после встречи точки останова вы- полнение программы будет продолжено автоматически. Если параметр bpnumber не задан, команда commands применяется к последней установлен- ной точке останова. condition bpnumber [condition] Добавляет условие condition к точке останова bpnumber. Параметр condition – это выражение, значение которого должно оцениваться как истинное, что- бы произошел останов в данной точке. Отсутствие параметра condition при- водит к сбросу всех условий, установленных ранее. Отладчик Python и модуль pdb 245 c(ont(inue)) Возобновляет выполнение программы, пока не будет встречена следующая точка останова. disable [bpnumber [bpnumber ...]] Деактивирует указанные точки останова. В отличие от команды clear, по- сле команды disable имеется возможность вновь активировать эти точки останова. d(own) Перемещает текущий кадр стека на один уровень вниз в стеке трассировки. enable [bpnumber [bpnumber ...]] Активирует указанные точки останова. h(elp) [command] Выводит список доступных команд. Если указана команда command, возвра- щает справочную информацию по этой команде. ignore bpnumber [count] Деактивирует точку останова на count проходов. j(ump) lineno Выполняет переход к следующей строке. Может использоваться только для перехода между инструкциями в одном кадре стека. Кроме того, не по- зволяет выполнить переход внутрь некоторых инструкций, например в се- редину цикла. l(ist) [first [, last]] Выводит листинг исходного программного кода. При использовании без аргументов эта команда выведет 11 строк, окружающих текущую строку (5 строк до и 5 строк после). При использовании с единственным аргумен- том она выведет 11 строк, начиная с указанной строки. При использовании с двумя аргументами – выведет строки из указанного диапазона. Если зна- чение параметра last меньше значения параметра first, оно будет интер- претироваться, как счетчик строк. n(ext) Выполняет инструкции до следующей строки в текущей функции. Если в текущей строке присутствуют вызовы других функций, они не учиты- ваются. p expression Вычисляет значение выражения expression в текущем контексте и выводит его. pp expression То же, что и команда p, но результат форматируется с использованием мо- дуля pprint. 246 Глава 11. Тестирование, отладка, профилирование и оптимизация q(uit) Выход из отладчика. r(eturn) Выполняет инструкции до момента выхода из текущей функции. run [args] Перезапускает программу с аргументами командной строки args , которые записываются в переменную sys.argv. Все точки останова и другие настрой- ки отладчика сохраняются. s(tep) Выполняет одну строку исходного программного кода и останавливает вы- полнение внутри вызываемых функций. tbreak [loc [, condition]] Устанавливает временную точку останова, которая удаляется после перво- го срабатывания. u(p) Перемещает текущий кадр стека на один уровень вверх в стеке трассиров- ки. unalias name Удаляет указанный псевдоним. until Продолжает выполнение программы, пока поток выполнения не покинет текущий кадр стека или пока не будет достигнута строка с номером, боль- ше чем у текущей. Например, если останов произошел в последней строке тела цикла, команда until продолжит выполнение всех инструкций, со- ставляющих цикл, пока он не завершится. w(here) Выведет трассировку стека. Отладка из командной строки Альтернативный метод отладки заключается в том, чтобы вызвать отлад- чик из командной строки. Например: % python -m pdb someprogram.py В данном случае отладчик будет запущен автоматически непосредствен- но перед запуском программы, что позволит установить точки останова и внести какие-либо изменения в настройки. Чтобы запустить программу, достаточно просто ввести команду continue. Например, если потребуется отладить функцию split() в программе, где она используется, это можно сделать, как показано ниже: % python –m pdb someprogram.py > /Users/beazley/Code/someprogram.py(1) Профилирование программы 247 -> import splitter (Pdb) b splitter.split Breakpoint 1 at /Users/beazley/Code/splitter.py:1 (Pdb) c > /Users/beazley/Code/splitter.py(18)split() -> fields = line.split(delimiter) (Pdb) Настройка отладчика Е сли в текущем каталоге или в домашнем каталоге пользователя присут- ствует файл .pdbrc, его содержимое будет прочитано и выполнено, как если бы строки из этого файла вводились в приглашении к вводу отладчика. Это может быть использовано для того, чтобы задать команды отладчика, которые желательно выполнять каждый раз, когда запускается отладчик (чтобы каждый раз не вводить эти команды вручную). Профилирование программы Для сбора профилирующей информации используются модули profile и cProfile. Оба модуля действуют совершенно одинаково, разница лишь в том, что модуль cProfile реализован как расширение на языке C, он ра- C, он ра- , он ра- ботает значительно быстрее и более современный. Оба модуля могут ис- пользоваться как для сбора общей информации (позволяющей выяснить, какие функции вызывались), так и для сбора статистических данных о производительности. Самый простой способ профилирования програм- мы заключается в том, чтобы запустить ее из командной строки, как по- казано ниже: % python -m cProfile someprogram.py Как вариант, можно использовать функцию из модуля profile: run(command [, filename]) Она выполняет содержимое строки command с помощью инструкции exec под управлением профилировщика. Аргумент filename – это имя файла для со- хранения первичных данных профилирования. Если этот аргумент отсут- ствует, отчет выводится в поток стандартного вывода. Результатом работы профилировщика является отчет, такой как приве- денный ниже: 126 function calls (6 primitive calls) in 5.130 CPU seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.030 0.030 5.070 5.070 121/1 5.020 0.041 5.020 5.020 book.py:11(process) 1 0.020 0.020 5.040 5.040 book.py:5(?) 2 0.000 0.000 0.000 0.000 exceptions.py:101(__init__) 1 0.060 0.060 5.130 5.130 profile:0(execfile(‘book.py’)) 0 0.000 0.000 profile:0(profiler) 248 Глава 11. Тестирование, отладка, профилирование и оптимизация Столбцы в отчете, сгенерированном функцией run(), интерпретируются следующим образом: Значение Описание primitive calls Количество нерекурсивных вызовов функций ncalls Общее число вызовов (включая рекурсивные) tottime Время, потраченное на выполнение этих функций (не включая вложенные вызовы функций) percall tottime / ncalls cumtime Общее время, потраченное на выполнение функций percall cumtime / primitive calls filename:lineno(function) Местонахождение и имя каждой функции Когда в первом столбце выводится два числа (например, “121/1”), второе из них – это число простых вызовов (primitive calls), а первое – фактическое число вызовов. В большинстве случаев для применения этого модуля бывает вполне доста- точно простого знакомства с отчетом, сгенерированным профилировщи- ком, например, когда требуется всего лишь определить, как распределяет- ся время выполнения программы. Однако если потребуется сохранить дан- ные и затем проанализировать их, можно воспользоваться модулем pstats. Более подробную информацию о том, как сохранять и анализировать дан- ные профилировщика, можно найти на странице http://docs.python.org/ library/profile.html Настройка и оптимизация В этом разделе рассматриваются некоторые основные правила, соблюдение которых позволит создавать программы, работающие быстрее и потреб- ляющие меньше памяти. Приемы, описываемые здесь, ни в коем случае не являются исчерпывающими, но они позволят программистам получить некоторую оценку своего программного кода. Измерение производительности Если вам потребуется узнать, как долго выполняется программа на языке Python, самый простой способ определить это состоит в том, чтобы запу- , самый простой способ определить это состоит в том, чтобы запу- стить ее под управлением какой-нибудь утилиты, напоминающей коман- ду time в UNIX. Если потребуется определить продолжительность работы блока инструкций, можно вставить вызовы функции time.clock(), которая возвращает текущее процессорное время, или time.time(), возвращающую текущее системное время. Например: start_cpu = time.clock() start_real= time.time() инструкции Настройка и оптимизация 249 инструкции end_cpu = time.clock() end_real = time.time() print(“Действительное время в сек. %f” % (end_real – start_real)) print(“Процессорное время в сек. %f” % (end_cpu - start_cpu)) Имейте в виду, что этот прием имеет смысл применять, только если фраг- мент программного кода, на котором производятся измерения, выполня- ется достаточно продолжительное время. Для измерения производитель- ности отдельных инструкций можно использовать функцию timeit(code [, setup]) из модуля timeit. Например: >>> from timeit import timeit >>> timeit(‘math.sqrt(2.0)’,’import math’) 0.20388007164001465 >>> timeit(‘sqrt(2.0)’,’from math import sqrt’) 0.14494490623474121 В этом примере первым аргументом функции timeit() передается про- граммный код, продолжительность работы которого требуется измерить. Второй аргумент – это инструкция, которая будет выполнена один раз для настройки окружения. Функция timeit() выполняет указанную инструк- цию один миллион раз и выводит время, потребовавшееся на ее выполне- ние. Количество повторений можно изменять с помощью именованного ар- гумента number=count функции timeit(). Кроме того, в модуле timeit имеется функция repeat(), которая также может использоваться для измерения производительности. Эта функция действу- ет точно так же, как функция timeit(), за исключением того, что она вы- полняет три серии измерений и возвращает список результатов. Например: >>> from timeit import repeat >>> repeat(‘math.sqrt(2.0)’,’import math’) [0.20306601524353027, 0.19715800285339355, 0.20907392501831055] >>> При измерении производительности обычно принято определять прирост скорости , который вычисляется, как частное от деления первоначального времени выполнения на новое время выполнения. Например, в предыду- щем эксперименте, где приводятся результаты измерения производитель- ности инструкций sqrt(2.0) и math.sqrt(2.0), прирост скорости составил 0,20388/0,14494 или примерно 1,41 раза. Иногда это число выражают в процентах, говоря, что прирост скорости составил примерно 41 %. Измерение объема потребляемой памяти В модуле sys имеется функция getsizeof(), которую можно использовать для определения объема памяти (в байтах), потребляемого отдельными объектами. Например: >>> import sys >>> sys.getsizeof(1) 14 250 Глава 11. Тестирование, отладка, профилирование и оптимизация >>> sys.getsizeof(“Hello World”) 52 >>> sys.getsizeof([1,2,3,4]) 52 >>> sum(sys.getsizeof(x) for x in [1,2,3,4]) 56 Для контейнерных объектов, таких как списки, кортежи и словари, воз- вращается размер памяти, занимаемой самим контейнером, а не суммар- ный объем памяти, занимаемой всеми элементами контейнера. Например, в предыдущем примере для списка [1,2,3,4] сообщается объем памяти, ко- торый в действительности меньше, чем требуется для хранения четырех целых чисел (на каждое число требуется 14 байт памяти). Это объясняет- ся тем, что содержимое списка не участвует в подсчетах. Для определения общего размера можно использовать функцию sum(), как показано здесь же. Помните, что функция getsizeof() может дать лишь общее представление об использовании памяти различными объектами. В действительности ин- терпретатор стремится многократно использовать одни и те же объекты, опираясь на механизм подсчета ссылок, поэтому фактический объем памя- ти, занимаемый объектом, может оказаться намного меньше, чем можно было бы представить. Кроме того, так как расширения Python, написан- Python, написан- , написан- ные на языке C, могут распределять память за пределами интерпретатора, может оказаться сложным точно определить общий объем используемой памяти. Поэтому другим приемом определения фактического объема зани- маемой памяти является исследование параметров выполняющегося про- цесса с помощью обозревателя системных процессов или диспетчера задач. Откровенно говоря, лучший способ получить представление об объеме за- нимаемой памяти состоит в том, чтобы сесть и проанализировать програм- му. Если известно, что программа выделяет память для различных струк- тур данных, а также известно, что это за структуры и какие данные будут храниться в них (целые числа, числа с плавающей точкой, строки и так далее), можно воспользоваться функцией getsizeof(), чтобы получить ис- ходные данные для вычисления максимального объема памяти, необходи- мой для работы программы, или, по крайней мере, получить достаточно информации для выполнения ориентировочноой оценки. Дизассемблирование Для дизассемблирования функций, методов и классов на языке Python в низкоуровневые инструкции интерпретатора можно использовать мо- дуль dis. В этом модуле имеется функция dis(), которая может использо- ваться, как показано ниже: >>> from dis import dis >>> dis(split) 2 0 LOAD_FAST 0 (line) 3 LOAD_ATTR 0 (split) 6 LOAD_FAST 1 (delimiter) 9 CALL_FUNCTION 1 12 STORE_FAST 2 (fields) ёё Настройка и оптимизация 251 3 15 LOAD_GLOBAL 1 (types) 18 JUMP_IF_FALSE 58 (to 79) 21 POP_TOP ёё 4 22 BUILD_LIST 0 25 DUP_TOP 26 STORE_FAST 3 (_[1]) 29 LOAD_GLOBAL 2 (zip) 32 LOAD_GLOBAL 1 (types) 35 LOAD_FAST 2 (fields) 38 CALL_FUNCTION 2 41 GET_ITER >> 42 FOR_ITER 25 (to 70) 45 UNPACK_SEQUENCE 2 48 STORE_FAST 4 (ty) 51 STORE_FAST 5 (val) 54 LOAD_FAST 3 (_[1]) 57 LOAD_FAST 4 (ty) 60 LOAD_FAST 5 (val) 63 CALL_FUNCTION 1 66 LIST_APPEND 67 JUMP_ABSOLUTE 42 >> 70 DELETE_FAST 3 (_[1]) 73 STORE_FAST 2 (fields) 76 JUMP_FORWARD 1 (to 80) >> 79 POP_TOP ёё 5 >> 80 LOAD_FAST 2 (fields) 83 RETURN_VALUE >>> Опытные программисты могут использовать эту информацию двумя спосо- бами. Во-первых, дизассемблированный листинг точно показывает, какие операции выполняются при работе функции. При внимательном изучении можно даже определить возможные способы оптимизации производитель- ности. Во-вторых, для тех, кто пишет многопоточные программы, будет полезно ознакомиться с таким листингом, так как в нем представлены отдельные инструкции интерпретатора, каждая из которых выполняется атомарно. То есть эта информация может быть полезна при исследовании сложных проблем, связанных с состоянием «гонки за ресурсами». Стратегии оптимизации В следующих разделах в общих чертах описываются некоторые стратегии оптимизации, которые, по опыту автора, хорошо зарекомендовали себя при разработке программ на языке Python. Изучите свою программу Прежде чем приступать к оптимизации чего бы то ни было, необходимо по- нять, какой вклад в общий прирост скорости даст оптимизация той или иной части программы. Например, если в 10 раз повысить скорость работы функции, время работы которой составляет 10 % от общего времени рабо- ты программы, общий прирост скорости составит примерно 9–10 %. В за- 252 Глава 11. Тестирование, отладка, профилирование и оптимизация висимости от усилий, которые придется приложить, может оказаться, что такая оптимизация не стоит затраченных сил. На первом этапе всегда желательно выполнить профилирование программ- ного кода, который предстоит оптимизировать. Основное внимание следует уделить функциям и методам, на выполнение которых тратится большая часть времени, а не сомнительным операциям, которые вызываются лишь время от времени. Изучите алгоритмы Даже неоптимально реализованный алгоритм O(n log n) выиграет соревно- O(n log n) выиграет соревно- (n log n) выиграет соревно- n log n) выиграет соревно- log n) выиграет соревно- log n) выиграет соревно- n) выиграет соревно- n) выиграет соревно- ) выиграет соревно- вание у высокооптимизированного алгоритма O(n 3 ). Нет смысла стараться оптимизировать неэффективные алгоритмы – лучше попробовать найти более удачный алгоритм. Используйте встроенные типы данных Встроенные типы данных, такие как кортежи, списки и словари, целиком реализованы на языке C и являются наиболее высокооптимизированными структурами данных в языке Python. Следует активно использовать эти типы для хранения и манипулирования данными в программе и старать- ся избегать создавать собственные структуры, имитирующие их поведение (например, двоичные деревья, связанные списки и так далее). При этом не следует забывать о типах в стандартной библиотеке. Неко- торые библиотечные модули объявляют новые типы данных, которые в определенных случаях выигрывают по производительности у встроен- ных типов. Например, тип collection.deque по своей функциональности напоминает списки, но он обладает более оптимизированной реализацией операции вставки новых элементов в оба конца. В отличие от него, список обеспечивает высокую эффективность только при добавлении элементов в конец. Когда новый элемент вставляется в начало списка, все остальные элементы сдвигаются, чтобы освободить место. Время, затрачиваемое на сдвиг элементов, тем больше, чем больше список. Только чтобы дать вам почувствовать разницу, ниже приводится время, затраченное на выполне- ние операции вставки одного миллиона элементов в начало списка и в на- чало объекта типа deque: >>> from timeit import timeit >>> timeit(‘s.appendleft(37)’, ... ‘import collections; s = collections.deque()’, ... number=1000000) 0.24434304237365723 >>> timeit(‘s.insert(0,37)’, ‘s = []’, number=1000000) 612.95199513435364 Не добавляйте лишние уровни абстракции Всякий раз, когда добавляется новый уровень абстракции или дополни- тельные удобства к функции или к объекту, это замедляет скорость работы программы. Следует стремиться сохранить баланс между удобством и про- изводительностью. Например, целью добавления нового уровня абстрак- ции зачастую является упрощение процесса программирования, что тоже важно. Настройка и оптимизация 253 В качестве простого примера рассмотрим программу, которая использует функцию dict() для создания словарей со строковыми ключами: s = dict(name=’GOOG’,shares=100,price=490.10) # s = {‘name’:’GOOG’, ‘shares’:100, ‘price’:490.10 } Этот прием может использоваться программистом, чтобы уменьшить объ- ем ввода с клавиатуры (в этом случае не придется вводить кавычки вокруг имен ключей). Однако этот альтернативный способ создания словаря ока- зывается более медленным, потому что в нем присутствует дополнитель- ный вызов функции. >>> timeit(“s = {‘name’:’GOOG’,’shares’:100,’price’:490.10}”) 0.38917303085327148 >>> timeit(“s = dict(name=’GOOG’,shares=100,price=490.10)”) 0.94420003890991211 Если в процессе своей работы ваша программа создает миллионы словарей, то вы должны знать, что первый вариант выполняется быстрее. За редким исключением, любая особенность, которая расширяет или изменяет прин- цип действия существующих объектов Python, будет более медленной. Помните, что классы и экземпляры основаны на применении словарей Экземпляры и классы, определяемые пользователем, основаны на приме- нении словарей. По этой причине операции поиска, изменения или удале- ния данных в экземпляре практически всегда выполняются медленнее, чем непосредственные операции со словарями. Если необходимо всего лишь создать структуру для хранения данных, применение словаря может оказаться более эффективным, чем создание класса. Только чтобы продемонстрировать разницу, ниже приводится пример простого класса, используемого для хранения информации о товарах на складе: class Stock(object): def __init__(self,name,shares,price): self.name = name self.shares = shares self.price = price Если сравнить производительность этого класса с производительностью словаря, можно получить весьма интересные результаты. Для начала срав- начала срав- начала срав- срав- срав- ним производительность операции создания экземпляров: >>> from timeit import timeit >>> timeit(“s = Stock(‘GOOG’,100,490.10)”,”from stock import Stock”) 1.3166780471801758 >>> timeit(“s = {‘name’ : ‘GOOG’, ‘shares’ : 100, ‘price’ : 490.10 }”) 0.37812089920043945 >>> Разница в скорости создания новых объектов составила примерно 3,5 раза. Далее рассмотрим скорость выполнения простых вычислений: 254 Глава 11. Тестирование, отладка, профилирование и оптимизация >>> timeit(“s.shares*s.price”, ... “from stock import Stock; s = Stock(‘GOOG’,100,490.10)”) 0.29100513458251953 >>> timeit(“s[‘shares’]*s[‘price’]”, ... “s = {‘name’ : ‘GOOG’, ‘shares’ : 100, ‘price’ : 490.10 }”) 0.23622798919677734 >>> Здесь разница в скорости составила примерно 1,2 раза. Суть здесь в том, что хотя вы можете объявлять новые объекты, используя классы, это не является единственным способом работы с данными. Кортежи и словари нередко являются достаточно удачным выбором. Их использование позво- лит повысить производительность программы и уменьшить объем потреб- ляемой памяти. Используйте атрибут __slots__ Если программа создает большое количество экземпляров пользователь- ских классов, имеет смысл подумать об использовании атрибута __slots__ в определении класса. Например: class Stock(object): __slots__ = [‘name’,’shares’,’price’] def __init__(self,name,shares,price): self.name = name self.shares = shares self.price = price Иногда атрибут __slots__ рассматривается как средство обеспечения без- опасности, потому что он ограничивает множество доступных имен атри- бутов. Однако в действительности это, скорее, средство оптимизации про- изводительности. Классы, в которых объявляется атрибут __slots__, не используют словарь для хранения данных экземпляра (для этих целей используется более эффективная внутренняя структура данных). Поэто- му экземпляры таких классов не только используют меньше памяти, но и обладают более эффективным способом доступа к данным. В некоторых случаях простое добавление атрибута __slots__, без внесения каких-либо других изменений, может обеспечить заметный прирост скорости. Однако по поводу использования атрибута __slots__ следует сделать одно предупреждение. Добавление его в определение класса может вызвать не- объяснимые нарушения в работе другого программного кода. Например, хорошо известно, что экземпляры хранят свои данные в словарях, доступ- ных в виде атрибута __dict__. Когда в объявление класса добавляется атри- бут __slots__, атрибут __dict__ становится недоступен, что вызывает нару- шения в работе программного кода, использующего его. Избегайте использования оператора (.) Всякий раз при попытке обращения к атрибуту объекта с помощью опе- ратора (.) выполняется поиск требуемого имени атрибута. Например, при обращении к x.name сначала выполняется поиск переменной с именем “x” в текущем окружении, а затем в объекте x производится поиск имени Настройка и оптимизация 255 “name” . В случае пользовательских объектов поиск имени атрибута может выполняться в словаре экземпляра, в словаре класса и в словарях базовых классов. Для вычислений, тесно связанных с использованием методов или функ- ций в других модулях, практически всегда лучше устранить этап поиска атрибутов, предварительно поместив требуемую операцию в локальную переменную. Например, если используется операция вычисления квадрат- ного корня, она будет выполняться быстрее, если импортирование будет выполнено как ‘from math import sqrt’, а затем будет вызываться функция ‘sqrt(x)’ , чем в случае, когда функция вызывается как ‘math.sqrt(x)’. В пер- вой части этого раздела было показано, что такой подход обеспечивает при- рост скорости примерно в 1.4 раза. Очевидно, что нет необходимости пытаться полностью избавиться от опе- ратора (.), так как это может существенно усложнить чтение программного кода. Однако для участков программы, где производительность имеет осо- бенно важное значение, этот прием может оказаться полезным. Используйте исключения для обработки нетипичных случаев Чтобы избежать ошибок, вы можете постараться добавить в программу до- полнительные проверки. Например: def parse_header(line): fields = line.split(“:”) if len(fields) != 2: raise RuntimeError(“Ошибка в заголовке”) header, value = fields return header.lower(), value.strip() Однако существует более простой способ обработки ошибок, который со- стоит в том, чтобы позволить программе самой генерировать исключения и обрабатывать их. Например: def parse_header(line): fields = line.split(“:”) try: header, value = fields return header.lower(), value.strip() except ValueError: raise RuntimeError(“Ошибка в заголовке”) Если проверить производительность обеих версий на корректно сформиро- ванных заголовках, вторая версия будет выполняться примерно на 10 про- центов быстрее. Блок try в случае, когда программный код не возбуждает исключение, выполняется быстрее, чем инструкция if. Не используйте исключения для обработки типичных случаев Не следует использовать исключения для обработки типичных случаев. Например, допустим, что имеется программа, которая интенсивно рабо- 256 Глава 11. Тестирование, отладка, профилирование и оптимизация тает со словарем, но в большинстве случаев искомые ключи отсутствуют в словаре. Теперь рассмотрим два подхода к организации работы со слова- рем: # Подход 1 : Выполнение поиска и обработка исключения try: value = items[key] except KeyError: value = None ёё # Подход 2: Проверка наличия ключа и выполнение поиска if key in items: value = items[key] else: value = None Простое измерение производительности показывает, что в случае обраще- ния к несуществующему ключу второй подход выполняется быстрее более чем в 17 раз! Кроме того, второй подход выполняется почти в два раза бы- стрее, чем тот же алгоритм, но с использованием метода items.get(key), а об- условлено это тем, что оператор in выполняется быстрее, чем вызов метода. Применяйте приемы функционального программирования и итерации Генераторы списков, выражения-генераторы, функции-генераторы, сопро- граммы и замыкания оказываются намного эффективнее, чем думает боль- шинство программистов. Обработка данных с применением генераторов списков и выражений-генераторов выполняется существенно быстрее, чем программный код, выполняющий итерации по данным вручную и произ- водящий те же самые вычисления. Кроме того, эти операции выполняются намного быстрее, чем программный код, использующий такие функции, как map() и filter(). Генераторы позволяют не только повысить производи- тельность, но и более эффективно использовать память. Используйте декораторы и метаклассы Декораторы и метаклассы обеспечивают возможность изменять поведение функций и классов. При этом, так как они выполняются во время объяв- ления функции или класса, они могут дать немалый прирост производи- тельности, особенно если в программе имеется множество особенностей, которые могут включаться и выключаться. В главе 6 «Функции и функ- циональное программирование» приводится пример использования деко- ратора для подключения к функциям возможности журналирования со- бытий, который не оказывает отрицательного влияния на производитель- ность, когда журналирование отключено. |