def process_cities(filename):
····with open(filename, 'rt') as file:
········for line in file:
············line = line.strip()
············if 'quit' == line.lower():
················return
············country, city = line.split(',')
············city = city.strip()
············country = country.strip()
············print(city.title(), country.title(), sep=',')
if __name__ == '__main__':
····import sys
····process_cities(sys.argv[1])
Запустим программу еще раз:
$ python capitals2.py cities2.csv
Buenos Aires,Argentina
La Paz,Bolivia
Brasilia,Brazil
Santiago,Chile
Bogotá,Colombia
Quito,Ecuador
Stanley,Falkland Islands
Cayenne,French Guiana
Georgetown,Guyana
Asunción,Paraguay
Lima,Peru
Paramaribo,Suriname
Montevideo,Uruguay
Caracas,Venezuela
Только что мы кратко рассмотрели отладчик — этого достаточно, чтобы показать вам, что вы можете сделать и какие команды будете использовать бо́льшую часть времени.
Помните: больше тестов — меньше отладки.
Записываем в журнал сообщения об ошибках
В какой-то момент вам может понадобиться перейти от использования выражений print() к записи сообщений в журнал. Журнал, как правило, представляет собой системный файл, в котором накапливаются сообщения, содержащие полезную информацию вроде временной метки или имени пользователя, запустившего программу. Зачастую журналы ежедневно ротируются (переименовываются) и сжимаются, благодаря чему они не переполняют ваш диск и не создают проблем. Если у вашей программы что-то пошло не так, вы можете просмотреть соответствующий файл журнала, чтобы увидеть, что произошло. Содержимое исключений особенно полезно записывать в журнал, поскольку оно подсказывает вам номер строки, после выполнения которой программа завершилась, и причину такого завершения.
Для журналирования используется модуль стандартной библиотеки logging (http://bit.ly/py-logging). Большинство его описаний я считаю немного непонятными. Спустя некоторое время эти описания будут казаться осмысленными, но в первый раз они выглядят чересчур сложными. Модуль logging содержит следующие концепции:
• сообщение, которое вы хотите сохранить в журнал;
• уровни приоритета и соответствующие функции — debug(), info(), warn(), error() и critical();
• один или несколько объектов журналирования для основной связи с модулем;
• обработчики, которые направляют значение в терминал, файл, базу данных или куда-нибудь еще;
• средства форматирования выходных данных;
• фильтры, которые принимают решения в зависимости от входных данных.
Рассмотрим простейший пример журналирования — просто импортируем модуль и воспользуемся некоторыми из его функций:
>>> import logging
>>> logging.debug("Looks like rain")
>>> logging.info("And hail")
>>> logging.warn("Did I hear thunder?")
WARNING: root: Did I hear thunder?
>>> logging.error("Was that lightning?")
ERROR: root: Was that lightning?
>>> logging.critical("Stop fencing and get inside!")
CRITICAL: root: Stop fencing and get inside!
Вы заметили, что вызовы debug() и info() не сделали ничего, а два других вывели на экран строку УРОВЕНЬ: root: перед каждым сообщением? Пока они выглядят как выражение print(), имеющее несколько личностей.
Но это полезно. Вы можете выполнить поиск определенного значения уровня в журнале, чтобы найти определенные сообщения, сравнить временны́е метки и увидеть, что случилось перед тем, как упал ваш сервер, и т. д.
Усиленные раскопки в документации отвечают на первую загадку (на вторую мы ответим уже через пару страниц): уровень приоритета по умолчанию — WARNING, он будет записан в журнал, когда мы вызовем первую функцию (logging.debug()). Мы можем указать уровень по умолчанию с помощью функции basicConfig(). Самый низкий уровень — DEBUG, это дает возможность поймать более высокие уровни:
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> logging.debug("It's raining again")
DEBUG: root: It's raining again
>>> logging.info("With hail the size of hailstones")
INFO: root: With hail the size of hailstones
Мы сделали это с помощью стандартных функций журналирования, не создавая специализированный объект. Каждый объект журналирования имеет имя. Создадим объект, который называется bunyan:
>>> import logging
>>> logging.basicConfig(level='DEBUG')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug('Timber!')
DEBUG: bunyan: Timber!
Если имя объекта журналирования содержит точки, они разделяют уровни иерархии таких объектов, каждый из которых потенциально имеет разные приоритеты. Это означает, что объект с именем quark выше, чем объект с именем quark.charmed. На вершине иерархии находится корневой объект журналирования с именем ' '.
До этого момента мы только выводили сообщения, что практически не отличается от функции print(). Чтобы направить сообщения в разные места назначения, используем обработчики. Самым распространенным местом назначения является файл журнала, направить туда сообщения можно так:
>>> import logging
>>> logging.basicConfig(level='DEBUG', filename='blue_ox.log')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug("Where's my axe?")
>>> logger.warn("I need my axe")
>>>
Ага, строки больше не показываются на экране, вместо этого они попадают в файл blue_ox.log:
DEBUG: bunyan: Where's my axe?
WARNING: bunyan: I need my axe
Вызов функции basicConfig() и передача имени файла в качестве аргумента создали для вас объект типа FileHandler и сделали его доступным объекту журналирования. Модуль журналирования содержит как минимум 15 обработчиков для отправки сообщений в разные места вроде электронной почты, веб-серверов, экранов и файлов.
Наконец, вы можете управлять форматом сообщений журнала. В нашем первом примере мы использовали формат, применяемый по умолчанию, в результате чего появилась следующая строка:
WARNING: root: Message…
Если вы предоставите строку format функции basicConfig(), то можете изменить формат по собственному желанию:
>>> import logging
>>> fmt = '%(asctime)s %(levelname)s %(lineno)s %(message)s'
>>> logging.basicConfig(level='DEBUG', format=fmt)
>>> logger = logging.getLogger('bunyan')
>>> logger.error("Where's my other plaid shirt?")
2014-04-08 23:13:59,899 ERROR 1 Where's my other plaid shirt?
Мы позволили объекту журналирования снова отправить выходные данные на экран, но изменили их формат. Модуль logging распознал количество имен переменных в строке формата fmt. Мы использовали asctime (дата и время как строка ISO 8601), levelname, lineno (номер строки) и само сообщение в переменной message. Существуют и другие встроенные переменные, вы можете предоставить и свои собственные переменные.
Пакет logging содержит гораздо больше особенностей, чем можно описать в этом небольшом обзоре. Вы можете писать в несколько журналов одновременно, указывая разные приоритеты и форматы. Этот пакет довольно гибок, но иногда это достигается за счет простоты.
Оптимизируем код
Обычно Python работает довольно быстро, однако иногда его скорости не хватает. В большинстве случаев вы можете ускорить работу, выбрав более качественный алгоритм или структуру данных. Идея заключается в том, чтобы знать, где это сделать. Даже опытные программисты ошибаются довольно часто. Вам нужно быть очень осторожными и семь раз отмерить, прежде чем отрезать. Это приводит нас к использованию таймеров.
Измеряем время
Вы уже видели, что функция time модуля time возвращает текущее время в формате epoch как число секунд с плавающей точкой. Быстрый способ засечь время — получить текущее время, что-то сделать, получить новое время и вычесть из него первое. Напишем соответствующий код и назовем файл time1.py:
from time import time
t1 = time()
num = 5
num *= 2
print(time() — t1)
В этом примере мы измеряем время, которое требуется на присвоение значения 5 переменной num и умножение его на 2. Этот пример не является реалистичным тестом производительности, это лишь пример того, как замерить время выполнения произвольного кода. Попробуйте запустить его несколько раз, чтобы увидеть, что время может варьироваться:
$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
1.9073486328125e-06
$ python time1.py
3.0994415283203125e-06
Программа работала две-три миллионные доли секунды. Попробуем выполнить что-то помедленнее, вроде функции sleep. Если мы усыпим выполнение на секунду, наш таймер покажет значение, чуть больше секунды. Сохраните файл под именем time2.py:
from time import time, sleep
t1 = time()
sleep(1.0)
print(time() — t1)
Чтобы быть уверенным в результатах, запустим программу несколько раз:
$ python time2.py
1.000797986984253
$ python time2.py
1.0010130405426025
$ python time2.py
1.0010390281677246
Как и ожидалось, программе для работы требуется около секунды. Если бы это оказалось не так, то либо нашему таймеру, либо функции sleep() должно было бы стать стыдно.
Существует более удобный способ измерить время выполнения фрагментов кода вроде этого — стандартный модуль timeit (http://bit.ly/py-timeit). У него имеется функция с именем, как вы уже догадались, timeit(), которая запустит ваш код заданное количество раз и выведет результаты. Ее синтаксис выглядит так: timeit.timeit(код, число, количество_раз).
В примерах этого раздела код должен находиться в кавычках, чтобы он выполнялся не после нажатия кнопки Return, а лишь внутри функции timeit(). (В следующем разделе вы увидите, как можно измерить время выполнения некоторой функции, передав ее имя в функцию timeit().) Запустим предыдущий пример и измерим время его выполнения. Назовем этот файл timeit1.py:
from timeit import timeit
print(timeit('num = 5; num *= 2', number=1))
Запустим его несколько раз:
$ python timeit1.py
2.5600020308047533e-06
$ python timeit1.py
1.9020008039660752e-06
$ python timeit1.py
1.7380007193423808e-06
Опять же эти две строки кода выполняются примерно за две миллионные доли секунды. Мы можем использовать аргумент repeat функции repeat() модуля timeit, чтобы выполнить код большее количество раз. Сохраните этот файл под именем timeit2.py:
from timeit import repeat
print(repeat('num = 5; num *= 2', number=1, repeat=3))
Попробуйте запустить его, чтобы увидеть what transpires:
$ python timeit2.py
[1.691998477326706e-06, 4.070025170221925e-07, 2.4700057110749185e-07]
Первый запуск занял две миллионные доли секунды, а второй и третий прошли быстрее. Почему? Для этого может быть много причин. Это могло произойти, например, потому, что мы тестировали очень небольшой фрагмент кода и скорость его выполнения зависит от того, что компьютер делал в эти моменты, как система Python оптимизировала вычисления, и от многого другого.
Или же это могла быть случайность. Попробуем сделать что-то более реалистичное, чем присвоение переменных и вызов функции sleep(). Мы измерим производительность, сравнив эффективность нескольких алгоритмов (программной логики) и структур данных (механизмов хранения).
Алгоритмы и структуры данных
Дзен Python (http://bit.ly/zen-py) гласит: «Должен существовать один, и желательно только один, очевидный способ сделать это». К сожалению, иногда он не является очевидным и вам приходится сравнивать альтернативные варианты. Например, что лучше использовать для создания списка: цикл for или включение списка? И что на самом деле значит «лучше»: быстрее, проще для понимания, менее затратно по ресурсам или более характерно для Python?
В следующем упражнении мы создадим список разными способами, сравнив скорость, читаемость и стиль. Перед вами файл time_lists.py:
from timeit import timeit
def make_list_1():
····result = []
····for value in range(1000):
········result.append(value)
····return result
def make_list_2():
····result = [value for value in range(1000)]
····return result
print('make_list_1 takes', timeit(make_list_1, number=1000), 'seconds')
print('make_list_2 takes', timeit(make_list_2, number=1000), 'seconds')
В каждой функции мы добавляем в список 1000 элементов и вызываем каждую функцию 1000 раз. Обратите внимание на то, что в этом тесте мы вызываем функцию timeit(), передавая ей имя функции в качестве первого аргумента вместо кода. Давайте ее запустим:
$ python time_lists.py
make_list_1 takes 0.14117428699682932 seconds
make_list_2 takes 0.06174145900149597 seconds
Включение списка отработало как минимум в два раза быстрее, чем добавление элементов в список с помощью функции append(). Как правило, включение быстрее, чем создание вручную.
Используйте эти идеи, чтобы сделать свой код быстрее.
Cython, NumPy и расширения C
Если вы усердно работаете, но все еще не можете достичь необходимой производительности, у вас есть и другие варианты.
Cython (http://cython.org/) — это гибрид языков Python и C, разработанный для преобразования Python: в скомпилированный код языка С внесены некоторые улучшения производительности. Эти аннотации относительно малы, они похожи на объявление типов некоторых переменных, аргументов функций или возвращаемых функциями значений. Подобные подсказки сделают научные вычисления, выполняющиеся в циклах, гораздо быстрее — в 1000 раз. Документацию и примеры можете найти в Cython wiki (https://github.com/cython/cython/wiki).
Из приложения В вы можете подробнее узнать о NumPy. Это математическая библиотека Python, написанная для ускорения на С.
Многие части Python и его стандартной библиотеки написаны на С для скорости и обернуты кодом на Python для удобства. При написании приложений эти приемы доступны и вам. Если вы знаете С и Python и действительно хотите, чтобы ваш код «летал», напишите расширение на языке С — это труднее, но улучшение оправдает затраченные усилия.
PyPy
Около 20 лет назад, когда язык Java только появился, он был медленным, как шнауцер, больной артритом. Но когда он стал дорого стоить компании Sun и прочим, они вложили миллионы в оптимизацию интерпретатора Java и лежащей в его основе виртуальной машины Java (Java Virtual Machine, JVM), заимствуя приемы из уже существовавших тогда языков Smalltalk и LISP. Компания Microsoft также вложила много усилий в оптимизацию своего языка C# и. NET VM.
Языком Python никто не владеет, поэтому никто так сильно не старается сделать его быстрее. Вы, возможно, используете стандартную реализацию Python. Она написана на С и часто называется CPython (не путать с Cython).
Как и языки PHP, Perl и даже Java, Python не компилируется в машинный код, он преобразуется в промежуточный язык (он называется байт-кодом или p-кодом), который затем интерпретирует виртуальная машина.
PyPy (http://pypy.org/) — это новый интерпретатор Python, который пользуется некоторыми приемами, ускорившими язык программирования Java. Тесты производительности интерпретатора (http://speed.pypy.org/) показывают, что PyPy в каждом тесте быстрее CPython в среднем в шесть раз и до 20 раз в отдельных случаях. Он работает с Python 2 и 3. Вы можете загрузить его и использовать вместо CPython. PyPy постоянно улучшается и однажды может заменить CPython. Чтобы узнать, подходит ли он вам, посетите его официальный сайт.
Управление исходным кодом