Упрощение кода приложений Python с помощью рефакторинга. Часть 1

Spread the love

Перевод статьи 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 основных аргумента:

  1. Тип анализа (cc для цикломатической сложности)
  2. Путь к файлу или папке для анализа

Выполните команду 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 операторов:

  1. if
  2. (
  3. )
  4. >
  5. :

Кроме того, есть 2 операнда:

  1. sys.argv
  2. 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 в обратном порядке дат. Вы увидите самый последний коммит вверху и самый старый в нижней части:

RevisionAuthorDateMILines of CodeCyclomatic Complexity
f37daf2Nate Prewitt2019-01-13100 (0.0)158 (0)9 (0)
6dd410fOfek Lev2019-01-13100 (0.0)158 (0)9 (0)
5c1f72eNate Prewitt2018-12-14100 (0.0)158 (0)9 (0)
c4d7680Matthieu Moy2018-12-14100 (0.0)158 (0)9 (0)
c452e3bNate Prewitt2018-12-11100 (0.0)158 (0)9 (0)
5a1e738Nate Prewitt2018-12-10100 (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.

Была ли вам полезна эта статья?
[2 / 4]

Spread the love
Подписаться
Уведомление о
guest
2 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Игорь
Игорь
5 лет назад

«Maintainability index» совсем плохо перевели. Мы все-таки в контексте создания кода, а не физических технических объектов