Простой Python — страница 42 из 66

Для написания программ, представленных в этой книге, я использовал текстовый интерфейс, но это не значит, что вы должны запускать весь код в консоли или текстовом окне. Существует множество бесплатных и коммерческих интегрированных сред разработки (Integrated Development Environment, IDE), которые являются графическими интерфейсами, поддерживающими инструменты вроде текстовых редакторов, отладчиков, поиска по библиотеке и т. д.

IDLE

IDLE (http://bit.ly/py-idle) — это IDE, предназначенная только для Python, которая поставляется со стандартным дистрибутивом. Она основана на интерфейсе tkinter и имеет простой GUI.

PyCharm

PyCharm (http://www.jetbrains.com/pycharm/) — это относительно новая графическая IDE, имеющая множество возможностей. Версия для сообщества бесплатна, также вы можете получить бесплатную лицензию для профессиональной версии, чтобы использовать ее для обучения или работы над проектом с открытым исходным кодом. На рис. 12.1 показан ее начальный экран.


Рис. 12.1. Начальный экран PyCharm

IPython

iPython (http://ipython.org/), которую вы увидите в приложении В, — это платформа для публикации приложений, а также IDE с широкими возможностями.

Именуйте и документируйте

Вы не вспомните то, что написали. Иногда случается так, что я смотрю на код, даже на тот, который написал недавно, и не понимаю, откуда он взялся. Именно поэтому полезно документировать собственный код. Документация может включать в себя комментарии и строки документации, также полезно давать переменным, функциям, модулям и классам осмысленные имена. Однако не перегибайте палку, как в этом примере:

>>> # Здесь я собираюсь присвоить значение 10 переменной "num":

… num = 10

>>> # Надеюсь, это сработало

… print(num)

10

>>> # Фух.

Вместо этого напишите, почему вы присвоили значение 10. Укажите, почему дали переменной именно имя num. Если вы пишете почтенный преобразователь температуры от шкалы Фаренгейта к шкале Цельсия, вам следует назвать переменные так, чтобы было понятно, что они делают, вместо того чтобы произвести на свет кучу волшебного кода. Небольшой тест также не повредит:

def ftoc(f_temp):

····"Convert Fahrenheit temperature  to Celsius and return it."

····f_boil_temp = 212.0

····f_freeze_temp = 32.0

····c_boil_temp = 100.0

····c_freeze_temp = 0.0

····f_range = f_boil_temp — f_freeze_temp

····c_range = c_boil_temp — c_freeze_temp

····f_c_ratio = c_range / f_range

····c_temp = (f_temp — f_freeze_temp) * f_c_ratio + c_freeze_temp

····return c_temp

if __name__ == '__main__':

····for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:

········c_temp = ftoc(f_temp)

········print('%f F => %f C' % (f_temp, c_temp))

Запустим тесты:

$ python ftoc1.py

-40.000000 F => -40.00000 °C

0.000000 F => -17.777778 C

32.000000 F => 0.00000 °C

100.000000 F => 37.777778 C

212.000000 F => 100.00000 °C

Мы можем сделать как минимум два улучшения.

• В языке Python нет констант, но таблица стилей PEP-8 рекомендует (http://bit.ly/pep-constant) использовать прописные буквы и подчеркивания (например, ALL_CAPS) при именовании переменных, которые должны считаться константами. Переименуем эти «константные» переменные в нашем примере.

• Поскольку мы заранее вычислили значения, основываясь на константах, перенесем их в верхнюю часть модуля. Таким образом, они будут рассчитываться только один раз при каждом вызове функции ftoc().

Так выглядит переделанный код:

F_BOIL_TEMP = 212.0

F_FREEZE_TEMP = 32.0

C_BOIL_TEMP = 100.0

C_FREEZE_TEMP = 0.0

F_RANGE = F_BOIL_TEMP — F_FREEZE_TEMP

C_RANGE = C_BOIL_TEMP — C_FREEZE_TEMP

F_C_RATIO = C_RANGE / F_RANGE

def ftoc(f_temp):

····"Convert Fahrenheit temperature  to Celsius and return it."

····c_temp = (f_temp — F_FREEZE_TEMP) * F_C_RATIO + C_FREEZE_TEMP

····return c_temp

if __name__ == '__main__':

····for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:

········c_temp = ftoc(f_temp)

········print('%f F => %f C' % (f_temp, c_temp))

Тестируем код

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

Самый простой способ протестировать программы, написанные на Python, — добавить команды print(). Read-Evaluate-Print Loop (REPL) интерактивного интерпретатора позволяет вам быстро изменять код и тестировать изменения. Однако в производственном коде выражения print() использовать не стоит, поэтому вам нужно помнить о том, что следует удалять их. К тому же ошибки, связанные с копированием и вставкой, сделать очень легко.

pylint, pyflakes и PEP-8

Следующим шагом перед созданием настоящих программ для тестирования является использование контролера кода Python. Самыми популярными являются pylint (http://www.pylint.org/) и pyflakes (http://bit.ly/pyflakes). Вы можете установить любой из них (или даже оба) с помощью pip:

$ pip install pylint

$ pip install pyflakes

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

a = 1

b = 2

print(a)

print(b)

print(c)

Так выглядит выходная информация от pylint:

$ pylint style1.py

No config file found, using default configuration

************* Module style1

C:··1,0: Missing docstring

C:··1,0: Invalid name "a" for type constant

··(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

C:··2,0: Invalid name "b" for type constant

··(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

E:··5,6: Undefined variable 'c'

Если пролистать дальше, к разделу Global evaluation, можно увидеть наш счет (10.0 — это высший балл):

Your code has been rated at -3.33/10

Ой! Сначала исправим ошибку. Строка вывода pylint, которая начинается с E, указывает на то, что найдена ошибка, которая заключается в том, что мы не присвоили значение переменной с до ее вывода на экран. Давайте это исправим:

a = 1

b = 2

c = 3

print(a)

print(b)

print(c)

$ pylint style2.py

No config file found, using default configuration

************* Module style2

C:··1,0: Missing docstring

C:··1,0: Invalid name "a" for type constant

··(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

C:··2,0: Invalid name "b" for type constant

··(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

C:··3,0: Invalid name "c" for type constant

··(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

Отлично, больше строк, начинающихся с Е, нет. Наш счет увеличился с -3.33 до 4.29:

Your code has been rated at 4.29/10

pylint хочет увидеть строку документации (короткий текстовый фрагмент в верхней части модуля или функции, описывающий код) и считает, что короткие имена переменных вроде a, b и c не очень аккуратны. Сделаем pylint счастливее, а наш код — еще лучше:

"Module docstring goes here"

def func():

····"Function docstring goes here. Hi, Mom!"

····first = 1

····second = 2

····third = 3

····print(first)

····print(second)

····print(third)

func()

$ pylint style3.py

No config file found, using default configuration

Жалоб нет. А какой у нас счет?

Your code has been rated at 10.00/10

Не так уж и плохо?

Еще одним контролером стиля является PEP-8 (https://pypi.python.org/pypi/pep8), вы можете установить его привычным способом:

$ pip install pep8

Что он скажет о нашей последней версии кода?

$ pep8 style3.py

style3.py:3:1: E302 expected 2 blank lines, found 1

Чтобы сделать код еще более стильным, он рекомендует добавить пустую строку после начальной строки документации модуля.

unittest

Мы убедились, что больше не оскорбляем чувство стиля богов кода, поэтому теперь можно перейти к настоящим тестам логики вашей программы.

Хорошим тоном является написание и запуск тестовых программ до отправки кода в систему контроля исходного кода. Написание тестов поначалу может быть утомительным, но они действительно помогают вам находить проблемы быстрее, особенно регрессионные тесты (суть которых заключается в том, чтобы сломать то, что раньше работало). Болезненный опыт учит всех разработчиков тому, что даже самое маленькое изменение, которое, по их заверениям, не затрагивает другие области приложения, на самом деле влияет на них. Если вы взглянете на качественные пакеты Python, то заметите, что они поставляются с набором тестов.

Стандартная библиотека содержит не один, а целых два пакета для тестирования приложений. Начнем с unittest (https://docs.python.org/3/library/unittest.html). Мы напишем модуль, который записывает слова с прописной буквы. Наша первая версия будет использовать стандартную строковую функцию capitalize(), что, как вы увидите, приведет к неожиданным результатам. Сохраните этот файл под именем cap.py:

def just_do_it(text):

····return text.capitalize()

Основная идея тестирования заключается в том, чтобы понять, какой результат вы хотите получить при определенных входных данных (в нашем примере вы хотите получить текст, который ввели, записанным с прописной буквы), отправить результат функции тестирования, а затем проверить, получен ли ожидаемый результат. Ожидаемый результат называется утверждением (assertion), поэтому в рамках пакета unittest вы проверяете результат с помощью методов, чьи имена начинаются со слова assert, например assertEqual, показанного в следующем примере.

Сохраните этот сценарий тестирования под именем test_cap.py:

import unittest

import cap

class TestCap(unittest.TestCase):

····def setUp(self):

········pass

····def tearDown(self):

········pass

····def test_one_word(self):

········text = 'duck'

········result = cap.just_do_it(text)

········self.assertEqual(result, 'Duck')

····def test_multiple_words(self):

········text = 'a veritable flock of ducks'

········result = cap.just_do_it(text)

········self.assertEqual(result, 'A Veritable Flock Of Ducks')

if __name__ == '__main__':

····unittest.main()

Перед каждым методом тестирования вызывается метод setUp(), а после каждого из методов тестирования — метод tearDown(). Задачей этих методов является выделение и освобождение внешних ресурсов, необходимых для тестов, вроде соединения с базой данных или создания некоторых тестовых данных. В нашем случае тесты автономны, и нам даже не нужно определять методы setUp() и tearDown(), однако создать их пустые версии не повредит. Сердцем наших тестов являются две функции с именами test_one_word() и test_multiple_words(). Каждая из них запускает определенную нами функцию just_do_it() с разными входными параметрами и проверяет, получен ли ожидаемый результат.

О’кей, запустим тест. Эта команда вызовет два наших метода тестирования:

$ python test_cap.py

F.

======================================================================

FAIL: test_multiple_words (__main__.TestCap)

------------------

Traceback (most recent call last):

···File "test_cap.py", line 20, in test_multiple_words

··self.assertEqual(result, 'A Veritable Flock Of Ducks')

AssertionError: 'A veritable flock of ducks'!= 'A Veritable Flock Of Ducks'

— A veritable flock of ducks

?···^·········^·····^··^

+ A Veritable Flock Of Ducks

?···^·········^·····^··^

------------------

Ran 2 tests in 0.001s

FAILED (failures=1)

Пакет устроил результат первой проверки (test_one_word), но не результат второй (test_multiple_words). Стрелки вверх (^) показывают, какие строки отличаются.

Что такого особенного в примере с несколькими словами? После прочтения документации для строковой функции capitalize (https://docs.python.org/3/library/stdtypes.html#str.capitalize) мы поняли причину проблемы: она увеличивает только первую букву первого слова. Возможно, нам сразу нужно было начать с чтения документации.

Нам нужна другая функция. После прочтения той страницы мы нашли функцию title() (https://docs.python.org/3/library/stdtypes.html#str.title). Изменим файл cap.py так, чтобы в нем вместо функции capitalize() использовалась функция title():

def just_do_it(text):

····return text.title()

Повторите тесты и взгляните на результат:

$ python test_cap.py

..

------------------

Ran 2 tests in 0.000s

OK

Все прошло отлично. Хотя на самом деле нет. Нам нужно добавить в файл test_cap.py как минимум еще один метод:

····def test_words_with_apostrophes(self):

········text = "I'm fresh out of ideas"

········result = cap.just_do_it(text)

········self.assertEqual(result, "I'm Fresh Out Of Ideas")

Запустите тесты еще раз:

$ python test_cap.py

..F

======================================================================

FAIL: test_words_with_apostrophes (__main__.TestCap)

------------------

Traceback (most recent call last):

···File "test_cap.py", line 25, in test_words_with_apostrophes

·····self.assertEqual(result, "I'm Fresh Out Of Ideas")

AssertionError: "I'M Fresh Out Of Ideas"!= "I'm Fresh Out Of Ideas"

— I'M Fresh Out Of Ideas

?···^

+ I'm Fresh Out Of Ideas

?···^

------------------

Ran 3 tests in 0.001s

FAILED (failures=1)

Наша функция увеличила букву m в конструкции I'm. В документации к функции title() мы обнаружили, что она плохо работает с апострофами. Нам действительно стоило сначала прочитать ее текст целиком.

В самом конце документации стандартной библиотеки, касающейся строк, мы находим еще одного кандидата — вспомогательную функцию с именем capwords(). Используем ее в файле cap.py:

def just_do_it(text):

····from string import capwords

····return capwords(text)

$ python test_cap.py

------------------

Ran 3 tests in 0.004s

OK

Наконец-то мы это сделали! Э-э-э, на самом деле нет. Нужно добавить еще один тест в файл test_cap.py:

····def test_words_with_quotes(self):

········text = "\"You're despicable,\" said Daffy Duck"

········result = cap.just_do_it(text)

········self.assertEqual(result, "\"You're Despicable,\" Said Daffy Duck")

Сработало?

$ python test_cap.py

…F

======================================================================

FAIL: test_words_with_quotes (__main__.TestCap)

------------------

Traceback (most recent call last):

···File "test_cap.py", line 30, in test_words_with_quotes

·····self.assertEqual(result, "\"You're

·····Despicable,\" Said Daffy Duck")

AssertionError: '"you\'re Despicable," Said Daffy Duck'

··!= '"You\'re Despicable," Said Daffy Duck'

— "you're Despicable," Said Daffy Duck

?··^

+ "You're Despicable," Said Daffy Duck

?··^

------------------

Ran 4 tests in 0.004s

FAILED (failures=1)

Выглядит так, будто первая двойная кавычка смутила даже функцию capwords, нашего текущего фаворита. Она попробовала увеличить символ " и уменьшить все остальное (You're). Нам также нужно проверить, оставила ли функция-увеличитель остальную часть строки нетронутой.

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

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

Пакет doctest

Вторым пакетом для тестирования стандартной библиотеки является doctest (http://bit.ly/py-doctest). С помощью этого пакета вы можете писать тесты внутри строки документации, которые и сами будут служить документацией. Он выглядит как интерактивный интерпретатор: символы >>>, за которыми следует вызов, а затем результаты в следующей строке. Вы можете запустить некоторые тесты в интерактивном интерпретаторе и просто вставить результат в свой тестовый файл. Мы модифицируем файл cap.py (убрав тот проблемный тест с кавычками):

def just_do_it(text):

····"""

····>>> just_do_it('duck')

····'Duck'

····>>> just_do_it('a veritable flock of ducks')

····'A Veritable Flock Of Ducks'

····>>> just_do_it("I'm fresh out of ideas")

····"I'm Fresh Out Of Ideas"

····"""

····from string import capwords

····return capwords(text)

if __name__ == '__main__':

····import doctest

····doctest.testmod()

Когда вы его запустите, в случае успеха он не выведет ничего:

$ python cap.py

Запустите его с опцией — v (verbose, verbose), чтобы увидеть, что произошло на самом деле:

$ python cap.py — v

Trying:

·····just_do_it('duck')

Expecting:

·····'Duck'

ok

Trying:

·····just_do_it('a veritable flock of ducks')

Expecting:

·····'A Veritable Flock Of Ducks'

ok

Trying:

·····just_do_it("I'm fresh out of ideas")

Expecting:

·····"I'm Fresh Out Of Ideas"

ok

1 items had no tests:

·····__main__

1 items passed all tests:

····3 tests in __main__.just_do_it

3 tests in 2 items.

3 passed and 0 failed.

Test passed.

Пакет nose

Сторонний пакет nose (https://nose.readthedocs.org/en/latest/) — это еще одна альтернатива пакету unittest. Команда, позволяющая установить его, выглядит так:

$ pip install nose

Вам не нужно создавать класс, который содержит тестовые методы, как мы делали при работе с unittest. Любая функция, содержащая в своем имени слово test, будет запущена. Модифицируем нашего последнего тестировщика Unittest и сохраним его под именем test_cap_nose.py:

import cap

from nose.tools import eq_

def test_one_word():

····text = 'duck'

····result = cap.just_do_it(text)

····eq_(result, 'Duck')

def test_multiple_words():

····text = 'a veritable flock of ducks'

····result = cap.just_do_it(text)

····eq_(result, 'A Veritable Flock Of Ducks')

def test_words_with_apostrophes():

····text = "I'm fresh out of ideas"

····result = cap.just_do_it(text)

····eq_(result, "I'm Fresh Out Of Ideas")

def test_words_with_quotes():

····text = "\"You're despicable,\" said Daffy Duck"

····result = cap.just_do_it(text)

····eq_(result, "\"You're Despicable,\" Said Daffy Duck")

Запустим тесты:

$ nosetests test_cap_nose.py

…F

======================================================================

FAIL: test_cap_nose.test_words_with_quotes

------------------

Traceback (most recent call last):

···File "/Users/…/site-packages/nose/case.py", line 198, in runTest

·····self.test(*self.arg)

···File "/Users/…/book/test_cap_nose.py", line 23, in test_words_with_quotes

·····eq_(result, "\"You're Despicable,\" Said Daffy Duck")

AssertionError: '"you\'re Despicable," Said Daffy Duck'

·····!= '"You\'re Despicable," Said Daffy Duck'

------------------

Ran 4 tests in 0.005s

FAILED (failures=1)

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

Другие фреймворки для тестирования

По некоторой причине людям нравится писать тестовые фреймворки для Python. Если вам любопытно, можете взглянуть на другие популярные решения вроде tox и py.test.

Постоянная интеграция

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

Эти системы велики, и я не буду рассматривать детали их установки и использования. Если они вам когда-нибудь понадобятся, вы будете знать, где их искать.

• buildbot (http://buildbot.net/). Эта система контроля версий, написанная на Python, автоматизирует построение, тестирование и выпуск кода.

• jenkins (http://jenkins-ci.org/). Система написана на Java, она выглядит наиболее предпочтительным инструментом для постоянной интеграции в данный момент.

• travis-ci (http://travis-ci.com/). Эта система автоматизирует проекты, размещенные на GitHub, она бесплатна для проектов с открытым исходным кодом.

Отлаживаем свой код