KVERNER
Matlab Simulink Python Java HELP Работы программиста профессионала
10 полезных советов по написанию скриптов Python
10 полезных советов по написанию скриптов Python
25.02.2023 Admin Комментарии 1 комментарий
Хотите несколько полезных советов по скриптам Python? Читайте дальше, чтобы получить совет от профессионала!
В этой статье я дам вам несколько советов и приемов Python, а также примеры скриптов, которые помогут вам максимально эффективно использовать Python. Во-первых, давайте определим, что такое скрипт Python. Сценарий Python — это набор инструкций, написанных на Python, чтобы попросить компьютер выполнить некоторые задачи.nВ этой статье мы начнем с простого варианта использования и усовершенствуем наш сценарий, реализуя различные советы по написанию полезных сценариев Python.
pip install opencv-python
Для этого примера я загрузил 50 изображений автомобилей Формулы-1, сохраненных в data/formula_one с файлами от 000000.jpg до 000049.jpg.
import cv2 import glob image_files = sorted(glob.glob("./data/formula_one/*.jpg")) for i in image_files: img = cv2.imread(i) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_img_small = cv2.resize(gray_img, (960, 540)) cv2.imshow('Grayscale Image', gray_img_small) cv2.waitKey(0) cv2.destroyAllWindows()
Ниже приведен пример результатов:

Теперь, когда у нас что-то работает, давайте посмотрим, как мы можем улучшить этот скрипт с помощью первого совета по скрипту Python.
1. Подчеркните читабельность кода Python
Мой первый совет — не пишите скрипты для себя, даже если вы единственный, кто их использует. Вместо этого подумайте о себе в будущем — или, если вы пишете код Python для кого-то другого, о человеке, который будет поддерживать код после вас. Другими словами, ваш код должен быть хорошо прокомментирован, удобочитаем и понятен любому, кто взглянет на ваш скрипт Python. Если вы решите перепроектировать свой код и сделать его максимально оптимизированным за счет удобочитаемости, вы должны быть тщательными в своих комментариях.
Я не добавлял комментариев к своему предыдущему примеру, так что это может быть первым шагом к улучшению нашего скрипта Python. Давайте воспользуемся комментариями, чтобы объяснить, что делает наш код:
import cv2 import glob # Load the images in ascending order image_files = sorted(glob.glob("./data/formula_one/*.jpg")) # Loop through the files display them as grayscale for i in image_files: img = cv2.imread(i) # Read the image gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Convert the image to grayscale gray_img_small = cv2.resize(gray_img, (960, 540)) # Resize the image for visualization purpose cv2.imshow('Grayscale Image', gray_img_small) # Display the gray image in a window called 'Grayscale Image' cv2.waitKey(0) cv2.destroyAllWindows() # Close all the open windows
Теперь наш код стал намного понятнее. Позже эти комментарии помогут вам вспомнить ход мыслей при написании кода. Однако наши комментарии могут быть немного очевидными, поскольку эта статья представляет собой руководство по советам и приемам Python. В этом контексте комментарии часто объясняют «что» в коде; вместо этого часто рекомендуется писать «почему» в этом фрагменте кода. Например, я мог бы написать комментарии так:
import cv2 import glob image_files = sorted(glob.glob("./data/formula_one/*.jpg")) for i in image_files: img = cv2.imread(i) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_img_small = cv2.resize(gray_img, (960, 540)) cv2.imshow('Grayscale Image', gray_img_small) cv2.waitKey(0) cv2.destroyAllWindows()
2. Пишите повторно используемые функции Python
Если вы можете сделать свой код полностью пригодным для повторного использования с помощью функций, вы избавите себя от многих проблем. Итак, вместо того, чтобы писать инструкции прямо в вашем файле, попробуйте заключить их в функции. Это заставит вас обобщить код и сделать его более пригодным для повторного использования в будущих сценариях.
Давайте напишем функцию, которая принимает наш файл изображения в качестве аргумента и возвращает серое изображение без изменения размера. Мы поместим функцию в начало скрипта, сразу после импорта:
import cv2 import glob # Color to Gray Function def color2gray(filename): img = cv2.imread(filename) # Read the image gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Convert the image to grayscale gray_img_small = cv2.resize(gray_img, (960, 540)) # Resize the image for visualization purposes cv2.imshow('Grayscale Image', gray_img_small) # Display the gray image in a window called 'Grayscale Image' cv2.waitKey(0) # Press any key to close the window return gray_img # Load the images in ascending order image_files = sorted(glob.glob("./data/formula_one/*.jpg")) # Loop through the files and display the images as grayscale for i in image_files: color2gray(i) cv2.destroyAllWindows() # Close all the open windows
3. Напишите свой собственный пакет Python
Мой следующий совет является продолжением предыдущего. Теперь, когда вы уже начали делать свой код пригодным для повторного использования, добавляя функции, вы можете сохранить эти функции в файле Python. Вы можете импортировать этот файл, как любую библиотеку, и использовать свои функции всякий раз, когда они вам понадобятся.
Следующим шагом будет организация вашего кода в класс. Это также может позволить вам использовать метод dir() для доступа к списку функций в вашем файле.
Давайте поместим нашу предыдущую функцию в новый файл Python с именем utils.py и импортируем его, как любой пакет Python. Помните, что ваш файл utils.py должен находиться в том же каталоге.
import cv2 # Color to Gray Function def color2gray(filename): img = cv2.imread(filename) # Read the image gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Convert the image to grayscale gray_img_small = cv2.resize(gray_img, (960, 540)) # Resize the image for visualization purposes cv2.imshow('Grayscale Image', gray_img_small) # Display the gray image in a window called 'Grayscale Image' cv2.waitKey(0) # Press any key to close the window return gray_img
И обновленный файл gray.py:
import cv2 import glob import utils # Load the images in ascending order image_files = sorted(glob.glob("./data/formula_one/*.jpg")) # Loop through the files display them as grayscale for i in image_files: utils.color2gray(i) cv2.destroyAllWindows() # Close all the open windows
Если по какой-то причине ваш файл utils.py находится в другом каталоге, вы можете импортировать его в свой файл gray.py:
import cv2 import glob import sys sys.path.append("your-path-to-utils") import utils # Load the images in ascending order image_files = sorted(glob.glob("./data/formula_one/*.jpg")) # Loop through the files display them as grayscale for i in image_files: utils.color2gray(i) cv2.destroyAllWindows() # Close all the open windows
О пакетах Python нужно знать больше, но это должно помочь вам начать. Удобочитаемость — одно из преимуществ хранения ваших функций в специальном файле Python, но это также упрощает поддержку вашего кода.
Вы можете получить доступ к списку своих функций с помощью dir().
import utils print(dir(utils))
Мы получаем следующий вывод:
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'color2gray', 'cv2']
Вы видите функцию color2gray, которую мы определили ранее. Но это не все. Далее давайте перейдем к одному из моих лучших советов и приемов по Python, когда мы рассмотрим модуль argparse.
4. Включите аргументы командной строки
Хотите сделать свой сценарий многоразовым? Обобщайте это! Какая проблема — копаться в вашем предыдущем скрипте Python, чтобы отредактировать его для вашего текущего варианта использования! Так что избавьте себя от хлопот и используйте argparse. Вы поблагодарите себя позже!
Argparse — мощная библиотека Python. Давайте обновим наш код, чтобы включить его и иметь возможность выполнять наш скрипт Python с аргументами командной строки.
import cv2 import glob import utils import argparse as ap # Define an argument parser object parser = ap.ArgumentParser() # Define the arguments to add parser.add_argument("-d", "--directory", type=str, required=True, help="Directory of files to load") parser.add_argument("-e", "--ext", type=str, default=".jpg", help="Define file extension") args = parser.parse_args() # Load the images in ascending order image_files = sorted(glob.glob(args.directory+"*"+args.ext)) # Loop through the files display them as grayscale for i in image_files: utils.color2gray(i) cv2.destroyAllWindows() # Close all the open windows
Мы можем запустить наш скрипт следующим образом:
python gray.py --directory ./data/formula_one/ --ext .jpg
Включение argparse в ваш скрипт делает его полностью пригодным для повторного использования. Используя его, вы можете вводить аргументы, необходимые для выполнения сценариев в соответствии с вашими потребностями, без непосредственного редактирования кода.
5. Используйте строку документации вместо однострочных комментариев
Далее я рекомендую использовать docstring вместо однострочных комментариев (т. е. комментариев со знаком #). Этому есть две причины. С одной стороны, вы можете легко и правильно писать многострочные комментарии; с другой стороны, пользователь программы может получить доступ к комментариям, вызвав функцию help().
Давайте отредактируем наш файл utils.py, включив в него комментарий к строке документации:
import cv2 # Color to Gray Function def color2gray(filename): """ This function reads an image and converts it to grayscale. For visualization purposes, we resize the output as a 960x540 image. Press any key to close the window and continue. """ img = cv2.imread(filename) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_img_small = cv2.resize(gray_img, (960, 540)) cv2.imshow('Grayscale Image', gray_img_small) cv2.waitKey(0) return gray_img print(help(color2gray))
Затем мы можем запустить наш utils.py в терминале…
python utils.py
… и получите документацию по функциям ниже:
Help on function color2gray in module __main__: color2gray(filename) This function reads an image and converts it to grayscale. For visualization purposes, we resize the output as a 960x540 image. Once the image is displayed, press any key to close the window and continue. (END)
6. Используйте контроль версий
У вас случалось, что ваш ноутбук умирал в ваших руках из-за того, что кто-то пролил на него чашку воды? Или вы потеряли все свои файлы, потому что допустили опечатку при запуске команды Linux rm -rf для удаления каталога? И то, и другое случилось со мной, так что вам не придется повторять мою ошибку!
Чтобы не переписывать сценарии и не брать отвертку, чтобы открыть ноутбук и извлечь жесткий диск, избавьте себя от проблем с контролем версий. Доступно несколько вариантов, таких как GitLab, GitHub, Beanstalk и другие. Я лично использую GitHub. Регулярная отправка кода гарантирует, что у вас всегда будет доступная версия проекта. Это можно сделать в мгновение ока:
- Перейдите в каталог вашего проекта и откройте терминал командной строки.
- Введите $ git init
- Введите $ git, добавьте имена файлов вашего проекта.
- Введите $ git фиксации
- Перейдите в свою учетную запись GitHub и создайте новый репозиторий.
- В терминале командной строки введите $ git remote add origin git@github.com:your-username/your-repo
- Введите $ git push
7. Сериализуйте свои переменные Python для повторного использования
Объектная сериализация также может быть удобной. Допустим, одна из ваших функций выполняется долго, и вам придется повторно использовать одни и те же выходные данные в нескольких файлах либо для исследовательских целей (например, проведения нескольких экспериментов), либо для какого-либо другого варианта использования. Вместо многократного запуска кода вы можете сериализовать вывод, сохранив его в виде файла рассола и перезагрузив в следующем сценарии.
В нашем предыдущем примере наша функция вернула изображение в градациях серого в виде массива NumPy; наш скрипт запускает 50 изображений. Возможно, у вас есть скрипт, выполняющий те же действия для тысяч изображений, и вам нужны эти массивы NumPy для другой цели. Мы можем сохранить эти структуры данных для повторного использования в будущем, чтобы избежать повторного запуска кода. Давайте импортируем pickle и обновим наш скрипт:
import cv2 import glob import utils import argparse as ap import pickle as pkl # Define an argument parser object parser = ap.ArgumentParser() # Define the arguments to add parser.add_argument("-d", "--directory", type=str, required=True, help="Directory of files to load") parser.add_argument("-e", "--ext", type=str, default=".jpg", help="Define file extension") args = parser.parse_args() # Load the images in ascending order image_files = sorted(glob.glob(args.directory+"*"+args.ext)) # Create an empty list gray_arr = [] # Loop through the files display them as grayscale for i in image_files: arr = utils.color2gray(i) gray_arr.append(arr) # Append the gray arrays to a list cv2.destroyAllWindows() # Close all the open windows # Save the gray images as a list of Numpy arrays in a pickle file. with open("gray_arr.pkl", "wb") as f: pkl.dump(gray_arr, f)
Теперь вы можете напрямую загрузить файл gray_arr.pkl и получить сохраненную структуру данных. Это может сэкономить ваше время, избегая повторного запуска сценария несколько раз для получения результатов. Это также может быть способом освободить память. Вместо того, чтобы хранить ваши значения в списке, вы можете сохранить их как файл рассола и удалить переменную, чтобы уменьшить нагрузку на вашу память.
8. Используйте оптимизированные библиотеки Python
Мой следующий важный совет взят непосредственно из предыдущего примера, где мы сохранили кучу изображений в виде массивов NumPy — библиотеки Python, написанной на C. C — гораздо более быстрый язык, чем Python. Но Python, несмотря на то, что он медленнее, прост в использовании и, как правило, ускоряет разработку, что объясняет его популярность и универсальность.
Используя библиотеки, написанные на C, вы делаете свой код Python намного проще и быстрее. Если ваше оборудование поддерживает Cuda, вы также можете использовать CuPy, версию NumPy, оптимизированную для графических процессоров, что еще больше ускорит ваш код. Эти библиотеки также очень удобны и позволяют выполнять сложные математические операции одним вызовом функции.
9. Пишите питонический код
Мой следующий совет — используйте однострочники Python, чтобы сделать ваш код более лаконичным и элегантным. Наш предыдущий код сохраняет выходные данные функции color2gray в традиционном списке:
# Create an empty list gray_arr = [] # Loop through the files display them as grayscale for i in image_files: arr = utils.color2gray(i) gray_arr.append(arr)
Однако мы можем переписать это на Pythonic как однострочник:
gray_arr = [utils.color2gray(i) for i in image_files]
Теперь наш код стал более лаконичным и элегантным — и, прежде всего, более питоновым!
10. Позаботьтесь о возможных ошибках
Когда ваш скрипт работает правильно и не выдает никаких ошибок, это здорово. Но это редко бывает. А иногда ошибки являются частью процесса и не должны останавливать ваш скрипт. Например, вы можете захотеть очистить некоторые изображения, но запрос выдает ошибку исключения и не может вернуть ни одного изображения. Затем ваш скрипт останавливается, не выполняя инструкций, которые вы просили. Таким образом, то, как вы ЗАВЕРШАЕТЕ СВОЙ СКРИПТ PYTHON, важно. Чтобы предотвратить остановку вашего скрипта, вы можете использовать try-except; в этом случае обработка исключений только пропустит файл и предотвратит остановку вашего скрипта.
5. Создание базовых скриптов#
Если говорить в целом, то скрипт — это обычный файл. В этом файле хранится последовательность команд, которые необходимо выполнить.
Начнем с базового скрипта. Выведем на стандартный поток вывода несколько строк.
Для этого надо создать файл access_template.py с таким содержимым:
access_template = ['switchport mode access', 'switchport access vlan <>', 'switchport nonegotiate', 'spanning-tree portfast', 'spanning-tree bpduguard enable'] print('\n'.join(access_template).format(5))
Сначала элементы списка объединяются в строку, которая разделена символом \n , а в строку подставляется номер VLAN, используя форматирование строк.
После этого надо сохранить файл и перейти в командную строку.
Так выглядит выполнение скрипта:
$ python access_template.py switchport mode access switchport access vlan 5 switchport nonegotiate spanning-tree portfast spanning-tree bpduguard enable
Ставить расширение .py у файла не обязательно, но, если вы используете Windows, то это желательно делать, так как Windows использует расширение файла для определения того, как обрабатывать файл.
В курсе все скрипты, которые будут создаваться, используют расширение .py. Можно сказать, что это «хороший тон» — создавать скрипты Python с таким расширением.
Cкрипты — Python: Настройка окружения
В интерпретируемых языках от написания кода до запуска — всего один шаг. Ничего не нужно компилировать в машинный код.
Всю работу делает интерпретатор, которому достаточно подать на вход скрипт — программу на интерпретируемом языке. Внутри этой программы записаны простые последовательности команд, которые компьютеру нужно выполнить.
Если на каком-то языке удобно писать скрипты, его называют «скриптовым языком» или «языком для написания сценариев».
Скрипты на Python
Python отлично подходит на роль скриптового языка. Последовательность команд в простых сценариях не нужно никак оформлять и запускать скрипты максимально просто. Мы просто пишем команды одну за другой в файл:
# file print('Hello, world!') print('This is a python-script!')
Затем мы просто вызываем интерпретатор с полученным файлом на входе:
У Python много полезных модулей и функций, входящих в поставку. Поэтому его часто используют для автоматизации различных задач, которые не хочется выполнять вручную при работе на компьютере. К тому же написание скриптов — отличная отправная точка для тех, кто только начинает знакомиться с программированием.
Скрипты и shebang
В Linux, macOS, BSD и других unix-подобных операционных системах командные оболочки умеют запускать скрипты на любых языках, в том числе и на Python. При этом скрипты сообщают оболочке, какой интерпретатор нужно вызывать для выполнения сценария.
Интерпретатор указывается специальной строкой в самой первой строчке файла скрипта, которая называется shebang. Это название произошло от первых двух символов такой строчки: # называется sharp, а ! — bang.
Типичный shebang выглядит так:
#!/usr/bin/python3
После символов #! идет путь до интерпретатора. При запуске скрипта с shebang командная оболочка читает первую строку и пробует запустить указанный интерпретатор. Если скрипту с указанным shebang дать права на исполнение, то интерпретатор в командной строке можно не указывать:
cat script.py #!/usr/bin/python3 print('Hello!') chmod +x script.py ./script.py Hello!
Shebang и разные версии Python
В целом shebang — штука довольно простая, когда интерпретатор в системе ровно один. Но мы с вами знаем, что версий Python в системе может быть установлено несколько. Более того, в виртуальном окружении путь к интерпретатору будет отличаться от /usr/bin и будет разным в разных окружениях.
Как сделать так, чтобы скрипт запускался всегда с нужной версией Python? Нужно всего лишь не указывать путь до команды python напрямую, а использовать программу env .
Эта программа умеет находить и запускать программы с учетом переменных окружения. При активации виртуального окружения модифицируется переменная $PATH , поэтому env будет запускать именно ту версию интерпретатора, которая нам нужна. Нужная версия просто найдется раньше, потому что путь до исполняемых файлов окружения добавляется в начало $PATH .
А теперь рассмотрим правильный способ указывать shebang в проектах на python:
#!/usr/bin/env python3 print('Hello!')
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Разработка надёжных Python-скриптов
Python — это язык программирования, который отлично подходит для разработки самостоятельных скриптов. Для того чтобы добиться с помощью подобного скрипта желаемого результата, нужно написать несколько десятков или сотен строк кода. А после того, как дело сделано, можно просто забыть о написанном коде и перейти к решению следующей задачи.
Если, скажем, через полгода после того, как был написан некий «одноразовый» скрипт, кто-то спросит его автора о том, почему этот скрипт даёт сбои, об этом может не знать и автор скрипта. Происходит подобное из-за того, что к такому скрипту не была написана документация, из-за использования параметров, жёстко заданных в коде, из-за того, что скрипт ничего не логирует в ходе работы, и из-за отсутствия тестов, которые позволили бы быстро понять причину проблемы.

При этом надо отметить, что превратить скрипт, написанный на скорую руку, в нечто гораздо более качественное, не так уж и сложно. А именно, такой скрипт довольно легко превратить в надёжный и понятный код, которым удобно пользоваться, в код, который просто поддерживать как его автору, так и другим программистам.
Автор материала, перевод которого мы сегодня публикуем, собирается продемонстрировать подобное «превращение» на примере классической задачи «Fizz Buzz Test». Эта задача заключается в том, чтобы вывести список чисел от 1 до 100, заменив некоторые из них особыми строками. Так, если число кратно 3 — вместо него нужно вывести строку Fizz , если число кратно 5 — строку Buzz , а если соблюдаются оба этих условия — FizzBuzz .
Исходный код
Вот исходный код Python-скрипта, который позволяет решить задачу:
import sys for n in range(int(sys.argv[1]), int(sys.argv[2])): if n % 3 == 0 and n % 5 == 0: print("fizzbuzz") elif n % 3 == 0: print("fizz") elif n % 5 == 0: print("buzz") else: print(n)
Поговорим о том, как его улучшить.
Документация
Я считаю, что полезно писать документацию до написания кода. Это упрощает работу и помогает не затягивать создание документации до бесконечности. Документацию к скрипту можно поместить в его верхнюю часть. Например, она может выглядеть так:
#!/usr/bin/env python3 """Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". """
В первой строке даётся краткое описание цели скрипта. В оставшихся абзацах содержатся дополнительные сведения о том, что именно делает скрипт.
Аргументы командной строки
Следующей задачей по улучшению скрипта станет замена значений, жёстко заданных в коде, на документированные значения, передаваемые скрипту через аргументы командной строки. Реализовать это можно с использованием модуля argparse. В нашем примере мы предлагаем пользователю указать диапазон чисел и указать значения для «fizz» и «buzz», используемые при проверке чисел из указанного диапазона.
import argparse import sys class CustomFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): pass def parse_args(args=sys.argv[1:]): """Parse arguments.""" parser = argparse.ArgumentParser( description=sys.modules[__name__].__doc__, formatter_class=CustomFormatter) g = parser.add_argument_group("fizzbuzz settings") g.add_argument("--fizz", metavar="N", default=3, type=int, help="Modulo value for fizz") g.add_argument("--buzz", metavar="N", default=5, type=int, help="Modulo value for buzz") parser.add_argument("start", type=int, help="Start value") parser.add_argument("end", type=int, help="End value") return parser.parse_args(args) options = parse_args() for n in range(options.start, options.end + 1): # .
Эти изменения приносят скрипту огромную пользу. А именно, параметры теперь надлежащим образом документированы, выяснить их предназначение можно с помощью флага —help . Более того, по соответствующей команде выводится и документация, которую мы написали в предыдущем разделе:
$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". positional arguments: start Start value end End value optional arguments: -h, --help show this help message and exit fizzbuzz settings: --fizz N Modulo value for fizz (default: 3) --buzz N Modulo value for buzz (default: 5)
Модуль argparse — это весьма мощный инструмент. Если вы с ним не знакомы — вам полезно будет просмотреть документацию по нему. Мне, в частности, нравятся его возможности по определению подкоманд и групп аргументов.
Логирование
Если оснастить скрипт возможностями по выводу некоей информации в ходе его выполнения — это окажется приятным дополнением к его функционалу. Для этой цели хорошо подходит модуль logging. Для начала опишем объект, реализующий логирование:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
Затем сделаем так, чтобы подробностью сведений, выводимых при логировании, можно было бы управлять. Так, команда logger.debug() должна выводить что-то только в том случае, если скрипт запускают с ключом —debug . Если же скрипт запускают с ключом —silent — скрипт не должен выводить ничего кроме сообщений об исключениях. Для реализации этих возможностей добавим в parse_args() следующий код:
# В parse_args() g = parser.add_mutually_exclusive_group() g.add_argument("--debug", "-d", action="store_true", default=False, help="enable debugging") g.add_argument("--silent", "-s", action="store_true", default=False, help="don't log to console")
Добавим в код проекта следующую функцию для настройки логирования:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
Основной код скрипта при этом изменится так:
if __name__ == "__main__": options = parse_args() setup_logging(options) try: logger.debug("compute fizzbuzz from <> to <>".format(options.start, options.end)) for n in range(options.start, options.end + 1): # .. except Exception as e: logger.exception("%s", e) sys.exit(1) sys.exit(0)
Если скрипт планируется запускать без прямого участия пользователя, например, с помощью crontab , можно сделать так, чтобы его вывод поступал бы в syslog :
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: if not sys.stderr.isatty(): facility = logging.handlers.SysLogHandler.LOG_DAEMON sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) sh.setFormatter(logging.Formatter( "[]: %(message)s".format( logger.name, os.getpid()))) root.addHandler(sh) else: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
В нашем небольшом скрипте неоправданно большим кажется подобный объём кода, нужный только для того, чтобы воспользоваться командой logger.debug() . Но в реальных скриптах этот код уже таким не покажется и на первый план выйдет польза от него, заключающаяся в том, что с его помощью пользователи смогут узнавать о ходе решения задачи.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
Тесты
Модульные тесты — это полезнейшее средство для проверки того, ведёт ли себя приложения так, как нужно. В скриптах модульные тесты используют нечасто, но их включение в скрипты значительно улучшает надёжность кода. Преобразуем код, находящийся внутри цикла, в функцию, и опишем несколько интерактивных примеров её использования в её документации:
def fizzbuzz(n, fizz, buzz): """Compute fizzbuzz nth item given modulo values for fizz and buzz. >>> fizzbuzz(5, fizz=3, buzz=5) 'buzz' >>> fizzbuzz(3, fizz=3, buzz=5) 'fizz' >>> fizzbuzz(15, fizz=3, buzz=5) 'fizzbuzz' >>> fizzbuzz(4, fizz=3, buzz=5) 4 >>> fizzbuzz(4, fizz=4, buzz=6) 'fizz' """ if n % fizz == 0 and n % buzz == 0: return "fizzbuzz" if n % fizz == 0: return "fizz" if n % buzz == 0: return "buzz" return n
Проверить правильность работы функции можно с помощью pytest :
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [100%] ========================== 1 passed in 0.05 seconds ==========================
Для того чтобы всё это заработало, нужно, чтобы после имени скрипта шло бы расширение .py . Мне не нравится добавлять расширения к именам скриптов: язык — это лишь техническая деталь, которую не нужно демонстрировать пользователю. Однако возникает такое ощущение, что оснащение имени скрипта расширением — это самый простой способ позволить системам для запуска тестов, вроде pytest , находить тесты, включённые в код.
В случае возникновения ошибки pytest выведет сообщение, указывающее на расположение соответствующего кода и на суть проблемы:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz FAILED [100%] ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101 >>> fizzbuzz(5, fizz=3, buzz=5) 102 'buzz' 103 >>> fizzbuzz(3, fizz=3, buzz=5) 104 'fizz' 105 >>> fizzbuzz(15, fizz=3, buzz=5) 106 'fizzbuzz' 107 >>> fizzbuzz(4, fizz=3, buzz=5) 108 4 109 >>> fizzbuzz(4, fizz=4, buzz=6) Expected: fizz Got: 4 /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================
Модульные тесты можно писать и в виде обычного кода. Представим, что нам нужно протестировать следующую функцию:
def main(options): """Compute a fizzbuzz set of strings and return them as an array.""" logger.debug("compute fizzbuzz from <> to <>".format(options.start, options.end)) return [str(fizzbuzz(i, options.fizz, options.buzz)) for i in range(options.start, options.end+1)]
В конце скрипта добавим следующие модульные тесты, использующие возможности pytest по использованию параметризованных тестовых функций:
# Модульные тесты import pytest # noqa: E402 import shlex # noqa: E402 @pytest.mark.parametrize("args, expected", [ ("0 0", ["fizzbuzz"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected
Обратите внимание на то, что, так как код скрипта завершается вызовом sys.exit() , при его обычном вызове тесты выполняться не будут. Благодаря этому pytest для запуска скрипта не нужен.
Тестовая функция будет вызвана по одному разу для каждой группы параметров. Сущность args используется в качестве входных данных для функции parse_args() . Благодаря этому механизму мы получаем то, что нужно передать функции main() . Сущность expected сравнивается с тем, что выдаёт main() . Вот что сообщит нам pytest в том случае, если всё работает так, как ожидается:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED [100%] ========================== 7 passed in 0.03 seconds ==========================
Если произойдёт ошибка — pytest даст полезные сведения о том, что случилось:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [. ] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________ args = '0 0', expected = ['0'] @pytest.mark.parametrize("args, expected", [ ("0 0", ["0"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected E AssertionError: assert ['fizzbuzz'] == ['0'] E At index 0 diff: 'fizzbuzz' != '0' E Full diff: E - ['fizzbuzz'] E + ['0'] fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py 125 DEBUG compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================
В эти выходные данные включён и вывод команды logger.debug() . Это — ещё одна веская причина для использования в скриптах механизмов логирования. Если вы хотите узнать подробности о замечательных возможностях pytest — взгляните на этот материал.
Итоги
Сделать Python-скрипты надёжнее можно, выполнив следующие четыре шага:
- Оснастить скрипт документацией, размещаемой в верхней части файла.
- Использовать модуль argparse для документирования параметров, с которыми можно вызывать скрипт.
- Использовать модуль logging для вывода сведений о процессе работы скрипта.
- Написать модульные тесты.
Вокруг этого материала развернулись интересные обсуждения — найти их можно здесь и здесь. Аудитория, как кажется, хорошо восприняла рекомендации по документации и по аргументам командной строки, а вот то, что касается логирования и тестов, показалось некоторым читателям «пальбой из пушки по воробьям». Вот материал, который был написан в ответ на данную статью.
Уважаемые читатели! Планируете ли вы применять рекомендации по написанию Python-скриптов, данные в этой публикации?
