Новости

11.12.2024

«Изучаем Python: программирование игр, визуализация данных, веб-приложения. 3е изд. дополненное и переработанное»

Вы ждали. Вы спрашивали. И наконец мы сделали!

Если вы хотели ворваться в программирование и освоить Python, то «Изучаем Python» может стать отличным стартом на этом увлекательном пути! Это не просто руководство — это проводник в мир программирования на Python. Он поможет вам заложить прочный фундамент для дальнейшего обучения и работы над своими собственными проектами.

В этой статье мы хотели бы рассказать про третье издание Эрика Мэтиза «Изучаем Python: программирование игр, визуализация данных, веб-приложения», дополненное и переработанное.

Идеальный старт для начинающих пайтонистов.

Об авторе
imageЭрик Мэтиз — автор самой популярной в мире книги по языку программирования Python. Преподает программирование и математику на Аляске. Начал писать программы с пяти лет. Эрик не считает себя учителем или писателем и больше всего любит проводить время на природе.


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

В книге «Изучаем Python» содержатся знания, которые необходимы для уверенного старта в программировании. Главы были переработаны и дополнены, чтобы соответствовать последним практикам программирования на Python: приемы редактирования в VS Code, применение модуля pathlib для работы с файлами, тестирование с помощью pytest, а также Matplotlib, Plotty и Django.

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

Содержание «Изучаем Python»

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

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

Чем третье переработанное и дополненное издание отличается от предыдущих

  • В первой главе представляется редактор VS Code, который пользуется популярностью как среди начинающих программистов, так и среди профессионалов, и работает на всех основных операционных системах.
  • Вторая глава посвящена новым методам `removeprefix()` и `removesuffix()`, которые будут полезны при работе с файлами и URL. Также в этой главе обсуждается улучшенный функционал системы обработки ошибок Python, которая теперь выводит гораздо больше конкретной информации, помогающую при отладке кода в случае возникновения ошибок.
  • Десятая глава охватывает модуль pathlib для работы с файлами. Так реализуется гораздо более простой подход к чтению и записи файлов.
  • Одиннадцатая глава сосредоточена на библиотеке `pytest`, которая служит стандартным инструментом для разработки автоматизированных тестов на Python. Она стала стандартным инструментом для написания тестов на Python. Ее интерфейс достаточно удобен для начинающих, а если вы продолжите карьеру программиста на Python, то будете использовать ее и в профессиональной среде.
  • В проекте «Инопланетное вторжение» в главах 12–14 была добавлена настройка, позволяющая управлять частотой кадров и обеспечивать стабильную работу игры на разных операционных системах. Подход к созданию флота пришельцев был упрощен, и общая организация проекта значительно улучшена.
  • В главах 15–17 проекты по визуализации данных используют последние возможности библиотек Matplotlib и Plotly. Обновленные настройки стилизации описаны для работы с Matplotlib. Проект случайного блуждания получил небольшое улучшение, что увеличило точность вывода графиков, поэтому вы увидите больше закономерностей, когда будете создавать новое блуждание. Во всех проектах с Plotly теперь используется модуль Plotly Express, который позволяет генерировать первичные визуализации всего за несколько строк кода. Вы сможете быстро просмотреть множество визуализаций, а затем сосредоточиться на доработке его отдельных элементов.
  • Главы 18–20 фокусируются на проекте «Журнал обучения», который строится на основе последних версий Django и Bootstrap. Некоторые компоненты проекта были переименованы для оптимизации структуры. Проект теперь развернут на платформе Platform.sh — современном хостинге для проектов Django. Конфигурационные файлы YAML позволяют тонко настроить процесс развертывания, что часто используется профессиональными программистами.
  • Приложение A полностью обновлено и предлагает рекомендации по установке Python на всех распространенных операционных системах. Приложение Б содержит детальные инструкции по настройке VS Code и краткий обзор современных редакторов кода и IDE. Приложение В предоставляет ссылки на наиболее популярные справочные онлайн-ресурсы. В приложении Г остается вводный мини-курс по использованию системы управления версиями Git. Приложение Д создано специально для этого издания и содержит руководство по устранению неполадок, которое окажется полезным, если процесс развертывания программ не удастся с первой попытки.

Если вы когда-либо задумывались: «А не заняться ли мне программированием?», то не раздумывайте больше! Книга «Изучаем Python» — ваш идеальный старт в это увлекательное путешествие. Вы сможете уверенно шагнуть в мир программирования, получив все необходимые инструменты и знания для успешного старта.

Погружайтесь в мир Python и начните свое путешествие уже сегодня!

Предлагаем ознакомиться с отрывком «Тестирование кода»

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

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

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

Установка pytest с помощью pip


Хотя Python содержит множество функций в стандартной библиотеке, разработчики Python также сильно зависят от пакетов сторонних разработчиков. Сторонний пакет (third-party package) — это библиотека, разработанная за пределами ядра языка Python. Некоторые популярные библиотеки сторонних разработчиков в конечном счете переходят в стандартную библиотеку и с этого момента добавляются в большинство установочных пакетов Python. Чаще всего это происходит с библиотеками, которые вряд ли сильно изменятся после того, как в них будут устранены первые ошибки. Такие библиотеки могут развиваться в том же темпе, что и весь язык.

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

Обновление pip


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

Откройте новое терминальное окно и выполните следующую команду:

   $ python -m pip install --upgrade pip
1 Requirement already satisfied: pip in /.../python3.11/site-packages (22.0.4)
--пропуск--
2 Successfully installed pip-22.1.2


Первая часть этой команды, python -m pip, дает Python указание запустить модуль pip. Вторая часть, install --upgrade, дает pip указание обновить ранее установленный пакет. Последняя часть, pip, — это имя стороннего пакета, который должен быть обновлен. Согласно выводу, на моем компьютере текущая версия pip, 22.0.4 «1», была заменена последней версией на момент написания книги, 22.1.2 «2».

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

$ python -m pip install --upgrade имя_пакета


ПРИМЕЧАНИЕ

В операционной системе Linux инструмент pip может быть не включен в Python. Если при попытке обновить pip вы получаете ошибку, то обратитесь к инструкциям, приведенным в приложении A.

Установка pytest


Теперь, обновив версию pip, мы можем установить pytest:

$ python -m pip install --user pytest
Collecting pytest
--пропуск--
Successfully installed attrs-21.4.0 iniconfig-1.1.1 ...pytest-7.x.x


Мы по-прежнему используем основную команду, pip install, однако на этот раз без флага --upgrade. Вместо этого мы используем флаг --user, давая Python указание установить этот пакет только для текущего пользователя. Согласно выводу, последняя версия pytest успешно установлена, как и ряд других пакетов, необходимых для работы pytest.

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

$ python -m pip install --user имя_пакета


ПРИМЕЧАНИЕ

Если у вас возникли трудности с выполнением этой команды, то попробуйте выполнить ее без флага --user.

Тестирование функции


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

name_function.py

def get_formatted_name(first, last):
"""Генерирует отформатированное полное имя."""
full_name = f"{first} {last}"
return full_name.title()


Функция get_formatted_name() формирует полное имя из имени и фамилии, разделив их пробелом, преобразует первый символ каждого слова в верхний регистр и возвращает полученный результат. Чтобы убедиться в том, что эта функция работает правильно, мы напишем программу, которая ее использует. Программа names.py запрашивает у пользователя имя и фамилию и выдает отформатированное полное имя:

names.py

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == 'q':
break
last = input("Please give me a last name: ")
if last == 'q':
break

formatted_name = get_formatted_name(first, last)
print(f"\tNeatly formatted name: {formatted_name}.")


Программа импортирует функцию get_formatted_name() из модуля name_function.py. Пользователь вводит последовательность имен и фамилий и видит, что программа сгенерировала отформатированные полные имена:

Enter 'q' at any time to quit.

Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin.

Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan.

Please give me a first name: q


Как видно из листинга, имена сгенерированы правильно. Но, допустим, вы решили изменить функцию get_formatted_name(), чтобы она также работала со вторыми именами. При этом необходимо проследить за тем, чтобы функция не перестала правильно работать для имен, состоящих только из имени и фамилии. Чтобы протестировать код, можно запустить names.py и для проверки вводить имя из двух компонентов (скажем, Janis Joplin) при каждом изменении get_formatted_name(), но это довольно утомительно. К счастью, Python предоставляет эффективный механизм автоматизации тестирования вывода функций. При автоматизации тестирования get_formatted_name() вы будете уверены в том, что функция успешно работает для всех видов имен, для которых написаны тесты.

Модульные тесты и тестовые сценарии


Существует множество подходов к тестированию программного обеспечения. Одним из самых простых видов тестирования является модульное тестирование. Модульный тест (unit test) проверяет правильность работы одного конкретного аспекта поведения функции. Тестовый сценарий (test case) представляет собой совокупность модульных тестов, которые совместно доказывают, что функция ведет себя так, как положено, во всем диапазоне ситуаций, которые она должна обрабатывать. Хороший тестовый сценарий учитывает все возможные виды ввода, которые может получать функция, и содержит тесты для представления всех таких ситуаций. Тестовый сценарий с полным покрытием (full coverage) содержит обширный спектр модульных тестов, охватывающих все возможные варианты использования функции. Обеспечение полного покрытия может быть весьма непростой задачей в крупном проекте. Часто бывает достаточно написать модульные тесты для критичных аспектов поведения вашего кода, а затем стремиться к полному покрытию только в том случае, если проект перейдет в фазу масштабного использования.

Прохождение теста


С помощью pytest создать модульный тест достаточно просто. Мы напишем одну тестовую функцию. Она будет вызывать тестируемую функцию, а мы — утверждать возвращаемое значение. Если наше утверждение верно, то тест пройдет; в противном случае — будет провален.

Вот тестовый сценарий, который проверяет, что функция get_formatted_name() работает правильно:

test_name_function.py

  from name_function import get_formatted_name

1 def test_first_last_name():
"""Поддерживаются ли имена типа 'Janis Joplin'?"""
2 formatted_name = get_formatted_name('janis', 'joplin')
3 assert formatted_name == 'Janis Joplin'


Прежде чем запустить тест, рассмотрим эту функцию. Имя файла с тестом очень важно; оно должно начинаться со слова test_. При запуске написанных нами тестов с помощью pytest этот модуль найдет все файлы, имена которых начинаются с test_, и запустит все содержащиеся в них тесты.

В файле с тестом мы сначала импортируем функцию, которую хотим протестировать: get_formatted_name(). Затем определяем тестовую функцию: в данном случае это test_first_last_name() «1». Это имя функции более длинное, чем использованное ранее, и на то есть веские причины. Так, тестовые функции должны начинаться со слова test, за которым следует символ подчеркивания. Все функции, имена которых начинаются с test_, будут определены модулем pytest и запущены в процессе тестирования.

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

Далее мы вызываем тестируемую функцию «2», в данном случае get_formatted _name() с аргументами 'janis' и 'joplin', точно так же, как и при запуске файла names.py. Результат выполнения этой функции мы присваиваем переменной formatted_name.

Наконец, мы создаем утверждение «3». Так мы утверждаем, что соблюдается то или иное условие. Здесь мы утверждаем, что переменной formatted_name должно быть присвоено значение 'Janis Joplin'.

Выполнение тестирования


Запустив файл test_name_function.py вручную, вы не получите результат, поскольку мы так и не вызвали тестовую функцию. Вместо этого мы попросим pytest запустить тестовый файл.

Для этого откройте терминальное окно и перейдите в папку, содержащую файл с тестом. В редакторе VS Code вы можете открыть папку, в которой находится файл с тестом, и использовать терминал, встроенный в окно редактора. В терминальном окне введите команду pytest. Вот что вы должны увидеть:

  $ pytest
========================= test session starts =========================
1 platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
2 rootdir: /.../python_work/chapter_11
3 collected 1 item

4 test_name_function.py . [100%]
========================== 1 passed in 0.00s ==========================


Разберем, что мы видим в выводе. Прежде всего здесь отображена информация о системе, в которой выполняется тест «1». Я тестирую программу в операционной системе macOS, так что в вашем случае вывод может быть несколько иным. Самое главное — указано, какие версии Python, pytest и других пакетов используются для выполнения теста.

Далее показан каталог, из которого запускается тест «2»: в моем случае это python_work/chapter_11. Далее указано, что pytest нашел один файл с тестом для запуска «3» и имя файла с тестом, который выполняется «4». Одна точка после имени файла информирует о том, что один тест пройден, а 100% говорит о том, что все тесты были запущены. В крупных проектах могут быть сотни и даже тысячи тестов, поэтому точки и индикатор завершенности в процентах пригодятся для отслеживания общего хода выполнения тестов.

Последняя строка говорит о том, что один тест пройден и на его выполнение ушло менее 0,01 секунды.

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

ПРИМЕЧАНИЕ

Если вы не знаете, как перейти в нужный каталог в терминале, то см. раздел «Запуск программ Python из терминала» в главе 1. А если выводится сообщение о том, что команда pytest не найдена, то вместо команды pytest используйте команду python -m pytest.

Сбой теста


Что произойдет при провале теста? Попробуем изменить функцию get_formatted_name(), чтобы она работала со вторыми именами, — но сделаем это так, чтобы она перестала работать с полными данными из имени и фамилии типа «Дженис Джоплин».

Новая версия get_formatted_name() с дополнительным аргументом второго имени выглядит так:

name_function.py

def get_formatted_name(first, middle, last):
"""Генерирует отформатированное полное имя."""
full_name = f"{first} {middle} {last}"
return full_name.title()


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

На этот раз pytest выдает следующий результат:

  $ pytest
========================= test session starts =========================
--пропуск--
1 test_name_function.py F [100%]
2 ============================== FAILURES ===============================
3 ________________________ test_first_last_name _________________________
def test_first_last_name():
"""Поддерживаются ли имена типа 'Janis Joplin'?"""
4 > formatted_name = get_formatted_name('janis', 'joplin')
5 E TypeError: get_formatted_name() missing 1 required positional
argument: 'last'

test_name_function.py:5: TypeError
======================= short test summary info =======================
FAILED test_name_function.py::test_first_last_name - TypeError:
get_formatted_name() missing 1 required positional argument: 'last'
========================== 1 failed in 0.04s ==========================


На этот раз информации гораздо больше, поскольку при сбое теста разработчик должен знать, почему это произошло. Вывод начинается с одной буквы F «1», которая сообщает, что один модульный тест в тестовом сценарии привел к ошибке. Далее приведен раздел FAILURES «2 », так как тесты, завершенные неудачно, обычно наиболее важны и на них следует обратить внимание при тестировании. Затем мы видим, что ошибка произошла в тесте test_first_last_name() «3». Угловая скобка «4» указывает на строку кода, которая привела к сбою тестирования. Буква E в следующей строке «5» отражает фактическую ошибку, которая привела к сбою: ошибку TypeError из-за отсутствия необходимого позиционного аргумента last. Наиболее важная информация повторяется в краткой выжимке в конце, поскольку при выполнении множества тестов программисту важно быстро понять, какие тесты провалились и почему.

Реакция на сбойный тест


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

В данном случае у функции get_formatted_name() было всего два обязательных параметра: имя и фамилия. Теперь она требует три обязательных параметра: имя, второе имя и фамилию. Добавление обязательного параметра для второго имени нарушило ожидаемое поведение get_formatted_name(). В таком случае лучше всего сделать параметр второго имени необязательным. После этого тесты для имен с двумя компонентами снова будут завершаться успешно, и программа сможет получать также вторые имена. Изменим функцию get_formatted_name(), чтобы параметр второго имени перестал быть обязательным, и снова выполним тестовый сценарий. Если он пройдет, то можно переходить к проверке правильности обработки вторых имен.

Чтобы сделать второе имя необязательным, нужно переместить параметр middle в конец списка параметров в определении функции и задать ему пустое значение по умолчанию. Будет добавлена еще и проверка if, которая правильно создает полное имя в зависимости от того, передается второе имя или нет:

name_function.py

def get_formatted_name(first, last, middle=''):
"""Создает отформатированное полное имя."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()


В новой версии функции get_formatted_name() параметр middle необязателен. Если второе имя передается функции, то полное будет содержать имя, второе имя и фамилию. В противном случае полное имя состоит только из имени и фамилии.

Теперь функция должна работать для обеих разновидностей имен. Чтобы узнать, работает ли функция для имен из двух компонентов типа Janis Joplin, снова запустите файл test_name_function.py:

$ pytest
========================= test session starts =========================
--пропуск--
test_name_function.py . [100%]
========================== 1 passed in 0.00s ==========================


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

Добавление новых тестов


Теперь мы знаем, что get_formatted_name() работает для простых имен, и можем написать второй тест для имен из трех компонентов. Для этого в файл test_name_function.py добавим еще одну тестовую функцию:

test_name_function.py

  from name_function import get_formatted_name

def test_first_last_name():
--пропуск--

def test_first_last_middle_name():
"""Поддерживаются ли такие имена, как 'Wolfgang Amadeus Mozart'?"""
1 formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
2 assert formatted_name == 'Wolfgang Amadeus Mozart'


Новой функции присваивается имя test_first_last_middle_name(). Имя должно начинаться со слова test_, чтобы эта функция выполнялась автоматически при запуске pytest. В остальном имя выбирается так, чтобы оно четко показывало, какое именно поведение get_formatted_name() мы тестируем. В результате при сбое теста вы сразу видите, к каким именам он относится.

Чтобы протестировать функцию, мы вызываем get_formatted_name() c тремя компонентами «1», после чего утверждаем «2», что возвращенное полное имя совпадает с ожидаемым. При повторном запуске pytest оба теста завершаются успешно:

  $ pytest
========================= test session starts =========================
--пропуск--
collected 2 items

1 test_name_function.py .. [100%]
========================== 2 passed in 0.01s ==========================


Две точки «1» означают, что два теста пройдены, что подтверждается в последней строке вывода. Отлично! Теперь мы знаем, что функция по-прежнему работает с именами из двух компонентов, как Janis Joplin, но можем быть уверены в том, что она сработает и для имен с тремя компонентами, таких как Wolfgang Amadeus Mozart.

УПРАЖНЕНИЯ

11.1. Город, страна. Напишите функцию, которая получает два параметра: название страны и название города. Функция должна возвращать одну строку в формате «Город, Страна» — например, Santiago, Chile. Сохраните функцию в модуле city_functions.py. в новой папке, чтобы pytest не выполнял тесты, которые мы уже написали.

Создайте файл test_cities.py для тестирования только что написанной функции. Напишите функцию test_city_country(), проверяющую, дает ли вызов функции с такими значениями, как 'santiago' и 'chile', правильную строку. Запустите test_cities.py и убедитесь в том, что тест test_city_country() проходит успешно.

11.2. Население. Измените свою функцию так, чтобы у нее был третий обязательный параметр — население. В новой версии функция должна возвращать одну строку вида «Город, Страна — население ххх», например, Santiago, Chile — population 5000000. Снова запустите тестирование. Убедитесь в том, что тест test_city_country() на этот раз не проходит.

Измените функцию так, чтобы параметр населения стал необязательным. Снова запустите тестирование и убедитесь в том, что тест test_city_country() снова проходит успешно.

Напишите второй тест test_city_country_population(), который проверяет вызов функции со значениями 'santiago', 'chile' и 'population=5000000'. Снова запустите тестирование и убедитесь в том, что новый тест завершается успешно.

Более подробно с книгой можно ознакомиться на сайте издательства.


Комментарии: 0

Пока нет комментариев


Оставить комментарий






CAPTCHAОбновить изображение

Наберите текст, изображённый на картинке

Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.