Упрощение кода приложений Python с помощью рефакторинга. Часть 1
Перевод статьи Anthony Shaw Refactoring Python Applications for Simplicity
В этой серии статей рассказано о способах измерения сложности кода и о том как избавиться от излишней сложности с помощью рефакторинга.
Вы хотите что бы ваш код всегда был прост и понятен? Вы начинаете проект с желанием того, что код будет чистый и с хорошей структурой. Но со временем в ваших приложениях происходят изменения, и все может стать сложным, запутанным и непонятным.
Если вы можете писать и поддерживать чистый и простой код, то это сэкономит вам много времени в долгосрочной перспективе. Когда ваш код хорошо продуман и прост в использовании, вам придется меньше времени тратить на тестирование, поиск ошибок и внесение изменений.
В этой статье вы узнаете:
- Как измерить сложность кода Python и ваших приложений
- Как изменить свой код, не нарушая его
- Каковы общие проблемы в коде на Python, которые вызывают дополнительную сложность и как их можно исправить
В этой статье я собираюсь использовать аналогию с сетью сетей путей метрополитена, чтобы рассказать о сложности программного обеспечения, потому что навигация в метро в большом городе так же может быть сложной и запутанной!
Сложность кода в Python
Сложность приложения и его кодовой базы зависит от задачи, которую оно выполняет. Если вы пишете код для лаборатории реактивного движения НАСА (буквально rocket science), то обычно это будет сложный код.
Вопрос не столько в том, «сложен ли мой код?», Сколько в том, что «насколько мой код сложнее, чем должен быть?»
Токийская железнодорожная сеть метрополитена является одной из самых обширных и сложных в мире. Отчасти это связано с тем, что Токио — это мегаполис с населением более 30 миллионов человек, а также потому, что три сети перекрывают друг друга.
Через центр Токио проходят скоростные транспортные сети Toei и Tokyo Metro, а также поезда Japan Rail East. Даже для самого опытного путешественника навигация по центру Токио может быть ошеломительно сложной.
Вот карта железнодорожной сети метрополитена Токио:
Если ваш код начинает выглядеть немного похожим на эту карту, то самое время почитать данное руководство.
Мы рассмотрим 4 показателя сложности, которые могут дать вам шкалу измерения относительного прогресса в миссии по упрощению кода:
Изучив метрики, вы узнаете об инструменте под названием wily для автоматизации расчета этих метрик.
Метрики для измерения сложности
Вот некоторые метрики сложности для языков программирования. Они применимы ко многим языкам, а не только к Python.
Количество строк кода
LOC ( Lines of Code), или Количество строк, является самой грубой мерой сложности. Это спорный вопрос , есть ли прямая связь между количеством строк кода и сложностью приложения, но косвенная корреляция очевидна. В конце концов, программа с 5 строками, вероятно, проще, чем программа с 5 миллионами.
При просмотре метрик Python мы стараемся игнорировать пустые строки и строки, содержащие комментарии.
Количество строк кода можно подсчитать с помощью команды wc в Linux и Mac OS, где file.py — это имя файла, который вы хотите измерить:
$ wc -l file.py
Если вы хотите подсчитать строки во всех файлах в папке путем рекурсивного поиска в файлах *.py, вы можете объединить wc с командой find:
$ find . -name \*.py | xargs wc -l
Для Windows PowerShell предлагается команда подсчета слов в Measure-Object и рекурсивный поиск файлов в Get-ChildItem:
$ Get-ChildItem -Path *.py -Recurse | Measure-Object –Line
В ответе вы увидите общее количество строк.
Почему строки кода используются для количественной оценки объема кода в вашем приложении? Предполагается, что строка кода примерно соответствует одной операцией.
В Python рекомендуется размещать по одной инструкции в каждой строке. Этот пример состоит из 9 строк кода:
x = 5 value = input("Enter a number: ") y = int(value) if x < y: print(f"{x} is less than {y}") elif x == y: print(f"{x} is equal to {y}") else: print(f"{x} is more than {y}")
Но если вы использовали только строки кода в качестве меры сложности, это будет совершенно неверный показатель.
Код Python должен быть легким для чтения и понимания. Взяв последний пример, вы можете уменьшить количество строк кода до 3:
x = 5; y = int(input("Enter a number:")) equality = "is equal to" if x == y else "is less than" if x < y else "is more than" print(f"{x} {equality} {y}")
Но этот результат трудно понять, и у PEP 8 есть рекомендации по максимальной длине строки и разрыву строки. Вы можете почитать, о том как писать красивый код Python с помощью PEP 8 (How to Write Beautiful Python Code With PEP 8), чтобы узнать больше о PEP 8.
Этот блок кода использует 2 функции языка Python, чтобы сделать код короче:
- Составные операции: использование ;
- Условные последовательности или тернарные операторы: name = value if condition else value if condition2 else value2
Мы сократили количество строк кода, но нарушили один из фундаментальных законов Python:
“Читаемость имеет значение”
— Тим Питерс, Дзен питона
Этот сокращенный код потенциально сложнее поддерживать, потому что сопровождающие кода — это люди, и этот короткий код труднее читать.
Далее мы рассмотрим более интересные и полезные метрики для сложности.
Цикломатическая Сложность
Цикломатическая сложность — это мера количества независимых путей кода в вашем приложении. Путь — это последовательность операторов, которой интерпретатор может следовать, чтобы добраться до конца приложения.
Один из способов думать о цикломатической сложности и путях кода — это представить, что ваш код в виде железнодорожной сети.
Для поездки вам может понадобиться смена поезда, чтобы добраться до пункта назначения. Железнодорожная система метрополитена Лиссабона в Португалии проста и удобна для навигации. Цикломатическая сложность любой поездки равна количеству линий, по которым вам нужно пройти:
Если вам нужно было добраться от Alvalade до Anjos, то вам нужно будет проехать 5 остановок на linha verde (зеленая линия):
Эта поездка имеет цикломатическую сложность 1, потому что вы берете только 1 поезд. Это легкая поездка. Этот поезд эквивалентен в этой аналогии одной ветви кода.
Если вам нужно было поехать из Аэропорта (Aeroporto), чтобы попробовать еду в районе Белен (Belém), то это более сложное путешествие. Вам придется пересесть на поезд в Alamedaand Cais do Sodré:
Эта поездка имеет цикломатическую сложность 3, потому что вы берете 3 поезда. Так что возможно лучше взять такси!
Учитывая, что вы не перемещаетесь по Лиссабону, а скорее всего пишете код, изменения железнодорожной линии превращаются в выполняемую ветвь, в виде оператора if.
Давайте рассмотрим этот пример:
x = 1
Существует только 1 способ выполнения этого кода, поэтому его цикломатическая сложность равна 1.
Если мы добавим решение или ответвление к коду в виде оператора if, это увеличит сложность:
x = 1 if x < 2: x += 1
Хотя существует только 1 способ выполнения этого кода, поскольку x является константой, этот код все равно имеет цикломатическую сложность 2. Все анализаторы цикломатической сложности будут обрабатывать if как ветвь.
Это также пример слишком сложного кода. Оператор if в данном случае бесполезен, так как x имеет фиксированное значение. Вы можете просто изменить этот пример следующим образом:
x = 2
Это был игрушечный пример, так что давайте рассмотрим что-то более реальное.
main() ниже имеет цикломатическую сложность 5. Я пометил (#n) каждую ветку в коде, чтобы вы могли видеть, где они находятся:
# cyclomatic_example.py import sys def main(): if len(sys.argv) > 1: # 1 filepath = sys.argv[1] else: print("Provide a file path") exit(1) if filepath: # 2 with open(filepath) as fp: # 3 for line in fp.readlines(): # 4 if line != "\n": # 5 print(line, end="") if __name__ == "__main__": # Ignored. main()
Конечно, есть способы, которыми код может быть преобразован в гораздо более простую альтернативу. Но мы вернемся к этому позже.
Примечание. Показатель цикломатической сложности был разработан Томасом Дж. МакКейбом, в 1976 году. Вы так же можете встретить упоминание этой метрики как метрика МакКейба (McCabe) или число МакКейба.
В следующих примерах мы будем использовать библиотеку radon
из PyPi для вычисления этой метрики. Вы можете установить его командой:
$ pip install radon
Чтобы вычислить цикломатическую сложность, используя radon, вы можете сохранить наш пример в файл cyclomatic_example.py и использовать radon из командной строки.
Команда radon принимает 2 основных аргумента:
- Тип анализа (cc для цикломатической сложности)
- Путь к файлу или папке для анализа
Выполните команду radon с аргументом cc для файла cyclomatic_example.py. Добавление аргумента -s отобразит цикломатическую сложность на экране:
$ radon cc cyclomatic_example.py -s cyclomatic_example.py F 4:0 main - B (6)
Вывод немного загадочный. Вот что означает каждая часть:
- F означает функцию, M означает метод, а C означает класс.
- main — это имя функции.
- 4 — строка, с которой начинается функция.
- B — оценка от A до F. A — лучшая оценка, то есть наименьшая сложность.
- Число в скобках, 6, является цикломатической сложностью кода.
Метрики Холстеда
Метрики сложности Холстеда относятся к размеру кодовой базы программы. Они были разработаны Морисом Х. Холстедом в 1977 году. В уравнениях Холстеда есть 4 меры:
- Операнды — это значения и имена переменных.
- Операторы — это все встроенные ключевые слова, например if, else, for или while.
- Длина (N) — это число операторов плюс количество операндов в вашей программе.
- Словарь (h) — это число уникальных операторов плюс количество уникальных операндов в вашей программе.
Затем есть 3 дополнительных показателя с этими показателями:
- Объем (Volume V) представляет собой произведение длины и словарного запаса.
- Сложность ( Difficulty D) представляет собой произведение половины уникальных операндов и повторного использования операндов.
- Усилие (Effort E) — это общая метрика, которая является продуктом объема и сложности.
Все это очень абстрактно, поэтому давайте сформулируем это в относительном выражении:
- Усилия вашего приложения будут самыми высокими, если вы используете много операторов и уникальных операндов.
- Усилие вашего приложения будет меньше, если вы используете меньше операторов и меньше переменных.
В примере cyclomatic_complexity.py операторы и операнды находятся в первой строке:
import sys # import (operator), sys (operand)
import — это оператор, а sys — это имя модуля, так что это операнд.
В немного более сложном примере могут быть несколько операторов и операндов:
if len(sys.argv) > 1: ...
В этом примере 5 операторов:
if
(
)
>
:
Кроме того, есть 2 операнда:
sys.argv
1
Помните, что radon учитывает только подмножество операторов. Например, круглые скобки исключаются при любых вычислениях.
Чтобы рассчитать меры Холстеда в radon, вы можете запустить следующую команду:
$ radon hal cyclomatic_example.py cyclomatic_example.py: h1: 3 h2: 6 N1: 3 N2: 6 vocabulary: 9 length: 9 calculated_length: 20.264662506490406 volume: 28.529325012980813 difficulty: 1.5 effort: 42.793987519471216 time: 2.377443751081734 bugs: 0.009509775004326938
Почему radon
дает метрику для времени (time) и ошибок (bugs)?
Холстед предположил, что вы можете оценить время (time), потраченное в секундах на кодирование, поделив усилие (effort E) на 18.
Холстед также заявил, что ожидаемое количество ошибок можно оценить, поделив объем (V) на 3000. Имейте в виду, что это было написано в 1977 году, еще до того, как Python был изобретен! Так что не паникуйте и просто начните искать ошибки.
Индекс поддерживаемости
Индекс поддерживаемости приводит показатели цикломатической сложности McCabe и объем Холстеда в масштабе примерно от нуля до ста.
Если вам интересно, оригинальное уравнение выглядит следующим образом:
В уравнении V — метрика объема Холстеда, C — цикломатическая сложность, а L — число строк кода.
Если вы так же озадачены, как и я, когда впервые увидели это уравнение, то вот что оно означает: оно рассчитывает масштаб, который включает в себя число переменных, операций, путей принятия решений и строк кода.
Это уравнение используется во многих инструментах и языках, поэтому это одна из самых стандартных метрик. Однако существует множество редакций этого уравнения, поэтому точное число не следует воспринимать как факт. radon, wily и Visual Studio ограничивают число от 0 до 100.
В шкале индекса поддерживаемости (Maintainability Index) все, на что нужно обращать внимание, это когда ваш код становится значительно ниже (ближе к 0). Шкала считает, что все, что ниже 25, трудно поддерживать, а что больше 75 — легко поддерживать. Индекс поддерживаемости (Maintainability Index) также называют MI.
Индекс поддерживаемости может быть использован в качестве меры для получения текущей поддерживаемости вашего приложения и проверки ваших успехов в процессе его рефакторинга.
Чтобы рассчитать индекс поддерживаемости по radon, выполните следующую команду:
$ radon mi cyclomatic_example.py -s cyclomatic_example.py - A (87.42)
В этом результате A — это оценка, которую Радон применил к числу 87,42 по своей шкале. По этой шкале A является наиболее поддерживаемым, а F — наименьшим.
Использование wily для получения и отслеживания сложности ваших проектов
wily — это библиотека с открытым исходным кодом, предназначенная для сбора метрик сложности кода, включая те, которые мы рассматривали до сих пор, такие как метрика Холстеда (Halstead), цикломатической сложности (Cyclomatic) и количества строк (LOC). wily интегрируется с Git и может автоматизировать сбор метрик в ветках и ревизиях Git.
Цель wily — дать вам возможность видеть тенденции и изменения в сложности вашего кода с течением времени. Если вы пытаетесь улучшить ваш автомобиль или улучшить свою физическую форму, вы должны начать с измерения базовой линии и далее отслеживать улучшения с течением времени.
Установка wily
wily доступен в PyPi и может быть установлен с помощью pip:
$ pip install wily
После установки wily в вашей командной строке появятся некоторые команды:
wily build
: перебирать историю Git и анализировать метрики для каждого файлаwily report
: увидеть историческую тенденцию в метриках для данного файла или папкиwily graph
: отобразить график набора метрик в файле HTML
Создание кеша
Прежде чем вы сможете использовать wily, вам нужно проанализировать свой проект. Это делается с помощью команды wily build.
В этом разделе руководства мы проанализируем очень популярный пакет requests, используемый для общения с HTTP API. Поскольку этот проект с открытым исходным кодом и доступен на GitHub, мы можем легко получить его и загрузить копию исходного кода:
$ git clone https://github.com/requests/requests $ cd requests $ ls AUTHORS.rst CONTRIBUTING.md LICENSE Makefile Pipfile.lock _appveyor docs pytest.ini setup.cfg tests CODE_OF_CONDUCT.md HISTORY.md MANIFEST.in Pipfile README.md appveyor.yml ext requests setup.py tox.ini
Примечание. Пользователям Windows следует использовать командную строку PowerShell для следующих примеров вместо традиционной командной строки MS-DOS. Чтобы запустить интерфейс командной строки PowerShell, нажмите Win + R, введите powershell, затем Enter.
Здесь вы увидите несколько папок для тестов, документации и конфигурации. Нас интересует только исходный код для пакета Python для requests.
Вызовите команду wily build из клонированного исходного кода и укажите имя папки с исходным кодом в качестве первого аргумента:
$ wily build requests
Это займет несколько минут, чтобы проанализировать, в зависимости от того, сколько ресурсов процессора имеет ваш компьютер:
Сбор данных о вашем проекте
После анализа исходного кода requests вы можете запросить любой файл или папку, чтобы увидеть ключевые показатели. Ранее в уроке мы обсуждали следующее:
- Строки кода
- Индекс поддерживаемости
- Цикломатическая Сложность
Это 3 метрики по умолчанию в wily. Чтобы просмотреть эти показатели для определенного файла (например, requests/api.py), выполните следующую команду:
$ wily report requests/api.py
wily распечатает табличный отчет по метрикам по умолчанию для каждого коммита Git в обратном порядке дат. Вы увидите самый последний коммит вверху и самый старый в нижней части:
Revision | Author | Date | MI | Lines of Code | Cyclomatic Complexity |
---|---|---|---|---|---|
f37daf2 | Nate Prewitt | 2019-01-13 | 100 (0.0) | 158 (0) | 9 (0) |
6dd410f | Ofek Lev | 2019-01-13 | 100 (0.0) | 158 (0) | 9 (0) |
5c1f72e | Nate Prewitt | 2018-12-14 | 100 (0.0) | 158 (0) | 9 (0) |
c4d7680 | Matthieu Moy | 2018-12-14 | 100 (0.0) | 158 (0) | 9 (0) |
c452e3b | Nate Prewitt | 2018-12-11 | 100 (0.0) | 158 (0) | 9 (0) |
5a1e738 | Nate Prewitt | 2018-12-10 | 100 (0.0) | 158 (0) | 9 (0) |
Это говорит нам о том, что файл requests/api.py имеет:
- 158 строки кода
- Идеальный показатель поддерживаемости 100
- Цикломатическая сложность 9
Чтобы увидеть другие метрики, сначала нужно узнать их названия. Вы можете увидеть это, выполнив следующую команду:
$ wily list-metrics
Вы увидите список операторов, модулей, которые анализируют код, и метрик, которые они предоставляют.
Чтобы запросить альтернативные метрики в команде отчета, добавьте их имена после имени файла. Вы можете добавить столько метрик, сколько пожелаете. Вот пример с рангом поддерживаемости и исходными строками кода:
$ wily report requests/api.py maintainability.rank raw.sloc
Вы увидите, что в таблице теперь есть 2 столбца с альтернативными метриками.
Графические Метрики
Теперь, когда вы знаете имена метрик и как их запрашивать в командной строке, вы также можете визуализировать их в виде графиков. wily поддерживает HTML и интерактивные диаграммы с интерфейсом, аналогичным команде report:
$ wily graph requests/sessions.py maintainability.mi
Ваш браузер по умолчанию откроется с интерактивной диаграммой, подобной этой:
Вы можете навести указатель мыши на определенные точки данных, и они покажут сообщение Git commit, а также данные.
Если вы хотите сохранить файл HTML в папке или хранилище, вы можете добавить флаг -o с путем к файлу:
$ wily graph requests/sessions.py maintainability.mi -o my_report.html
Теперь будет файл my_report.html, которым вы сможете поделиться с другими. Эта команда идеально подходит для командных панелей.
wily
и pre-commit
Wily может быть настроен так, что перед тем, как вы внесете изменения в свой проект, он может предупредить вас об улучшении или ухудшении сложности.
У wily есть команда wily diff, которая сравнивает последние проиндексированные данные с текущей рабочей копией файла.
Для запуска команды wily diff укажите имена файлов, которые вы изменили. Например, если я внес некоторые изменения в requests/api.py, вы увидите влияние на показатели, запустив wily diff с путем к файлу:
$ wily diff requests/api.py
В ответе вы увидите все измененные метрики, а также функции или классы, которые были изменены для цикломатической сложности:
Команда diff может быть связана с инструментом pre-commit. pre-commit вставляет хук в вашу конфигурацию Git, который вызывает скрипт каждый раз, когда вы запускаете команду git commit.
Для установки pre-commit вы можете установить из PyPI:
$ pip install pre-commit
Добавьте следующее в .pre-commit-config.yaml в корневом каталоге ваших проектов:
repos: - repo: local hooks: - id: wily name: wily entry: wily diff verbose: true language: python additional_dependencies: [wily]
После установки этого вы запускаете команду pre-commit install для завершения:
$ pre-commit install
Теперь всякий раз, когда вы запускаете команду git commit, она вызывает wily diff вместе со списком файлов, которые вы добавили в свои поэтапные изменения.
Wily — полезная утилита, позволяющая оценить сложность вашего кода и измерить улучшения, которые вы делаете, когда начинаете рефакторинг.
В следующей статье этой серии мы поговорим о рефакторинге в Python.
«Maintainability index» совсем плохо перевели. Мы все-таки в контексте создания кода, а не физических технических объектов
Да пожалуй, подобрал другой вариант «поддерживаемость»