Рефакторинг приложений Python для простоты

Рефакторинг приложений Python для простоты

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

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

В этом руководстве вы узнаете:

  • Как измерить сложность кода Python и ваших приложений

  • Как изменить свой код, не нарушая его

  • Каковы общие проблемы в коде Python, которые вызывают дополнительную сложность и как их можно исправить

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

Free Bonus:5 Thoughts On Python Mastery, бесплатный курс для разработчиков Python, который показывает вам план действий и образ мышления, который вам понадобится, чтобы вывести свои навыки Python на новый уровень.

Сложность кода в Python

Сложность приложения и его кодовой базы зависит от выполняемой им задачи. Если вы пишете код для лаборатории реактивного движения НАСА (буквальноrocket science), то это будет сложно.

Вопрос не так много: «Сложен ли мой код?» «Мой код сложнее, чем должен быть?»

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

Через центральный Токио проходят скоростные транспортные сети Тоейского и Токийского метрополитенов, а также поезда Japan Rail East. Даже для самого опытного путешественника навигация по центральному Токио может быть ошеломительно сложной.

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

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

Во-первых, мы рассмотрим 4 показателя сложности, которые могут дать вам шкалу для измерения вашего относительного прогресса в миссии по упрощению кода:

Code Complexity Metrics Table of Contents

Изучив метрики, вы узнаете об инструментеwily для автоматизации расчета этих показателей.

Метрики для измерения сложности

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

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

Вот некоторые метрики для языков программирования. Они применимы ко многим языкам, а не только к Python.

Строки кода

LOC, или Строки Кодекса, является самой грубой мерой сложности. Это спорно, есть ли прямая связь между строками коды и сложностью приложения, но косвенная корреляция очевидна. В конце концов, программа с 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 строк кода:

 1 x = 5
 2 value = input("Enter a number: ")
 3 y = int(value)
 4 if x < y:
 5     print(f"{x} is less than {y}")
 6 elif x == y:
 7     print(f"{x} is equal to {y}")
 8 else:
 9     print(f"{x} is more than {y}")

Если вы используете только строки кода в качестве меры сложности, это может привести к неправильному поведению.

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

 1 x = 5; y = int(input("Enter a number:"))
 2 equality = "is equal to" if x == y else "is less than" if x < y else "is more than"
 3 print(f"{x} {equality} {y}")

Но результат трудно понять, и в PEP 8 есть рекомендации по максимальной длине строки и разрыву строки. Вы можете проверитьHow to Write Beautiful Python Code With PEP 8, чтобы узнать больше о PEP 8.

Этот блок кода использует 2 функции языка Python, чтобы сделать код короче:

  • Compound statements: с использованием;

  • Связанные условные или троичные операторы: name = value if condition else value if condition2 else value2

Мы сократили количество строк кода, но нарушили один из фундаментальных законов Python:

«Читаемость имеет значение»

- Тим Питерс, дзен питона

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

Цикломатическая Сложность

Цикломатическая сложность - это мера количества независимых путей кода в вашем приложении. Путь - это последовательность операторов, которой интерпретатор может следовать, чтобы добраться до конца приложения.

Один из способов думать о цикломатической сложности и путях кода - представить, что ваш код подобен железнодорожной сети.

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

Если вам нужно добраться отAlvalade доAnjos, вы должны проехать 5 остановок поlinha verde (зеленая линия):

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

Если вам нужно было ехать изAeroporto (аэропорт), чтобы попробоватьfood in the district of Belém, то это более сложный путь. Вам нужно будет сменить поезда вAlameda иCais do Sodré:

Эта поездка имеет цикломатическую сложность 3, потому что вы берете 3 поезда. Тебе лучше взять такси!

Поскольку вы не путешествуете по Лиссабону, а пишете код, изменения линии поезда превращаются в выполняемую ветвь, как операторif. Давайте рассмотрим этот пример:

x = 1

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

Если мы добавим решение или ответвление в код в виде оператораif, это усложнит:

x = 1
if x < 2:
    x += 1

Несмотря на то, что существует только один способ выполнения этого кода, посколькуx является константой, его цикломатическая сложность равна 2. Все анализаторы цикломатической сложности будут рассматривать операторif как ветвь.

Это также пример чрезмерно сложного кода. Операторif бесполезен, посколькуx имеет фиксированное значение. Вы можете просто изменить этот пример следующим образом:

x = 2

Это был игрушечный пример, так что давайте рассмотрим что-то более реальное.

main() имеет цикломатическую сложность 5. Я прокомментирую каждую ветку в коде, чтобы вы могли видеть, где они находятся:

# 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()

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

Note: Показатель цикломатической сложности былdeveloped by Thomas J. McCabe, Sr в 1976 г. Вы можете увидеть это какMcCabe metric илиMcCabe number.

В следующих примерах мы будем использовать библиотекуradonfrom 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 меры:

  • Operands - значения и имена переменных.

  • Operators - это все встроенные ключевые слова, напримерif,else,for илиwhile.

  • Length (N) - это количество операторов плюс количество операндов в вашей программе.

  • Vocabulary (h) - это количество операторовunique плюс количество операндовunique в вашей программе.

Затем есть 3 дополнительных показателя с этими показателями:

  • Volume (V) представляет собой произведениеlength иvocabulary.

  • Difficulty (D) представляет собой произведение половины уникальных операндов и повторного использования операндов.

  • Effort (E) - это общая метрика, которая является произведениемvolume иdifficulty.

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

  • Усилия вашего приложения будут самыми высокими, если вы используете много операторов и уникальных операндов.

  • Усилие вашего приложения будет ниже, если вы используете несколько операторов и меньше переменных.

В примере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 дает метрику времени и ошибок?

Холстед предположил, что можно оценить время, затрачиваемое на код в секундах, разделив усилия (E) на 18.

Холстед также заявил, что ожидаемое количество ошибок можно оценить, разделив объем (V) на 3000. Имейте в виду, что это было написано в 1977 году, еще до того, как Python был изобретен! Так что не паникуйте и просто начните искать ошибки.

Индекс ремонтопригодности

Индекс ремонтопригодности приводит показатели цикломатической сложности McCabe и объем Холстеда в масштабе примерно от нуля до ста.

Если вам интересно, оригинальное уравнение выглядит следующим образом:

MI Equation

В уравненииV - метрика объема Холстеда,C - цикломатическая сложность, аL - количество строк кода.

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

Он используется во многих инструментах и ​​языках, поэтому это одна из самых стандартных метрик. Однако существует множество пересмотров уравнения, поэтому точное число не следует воспринимать как факт. radon,wily и Visual Studio ограничивают число от 0 до 100.

В шкале индекса ремонтопригодности все, на что вам нужно обращать внимание, это когда ваш код становится значительно ниже (ближе к 0). Шкала рассматривает все, что меньше 25, какhard to maintain, и все, что больше 75, какeasy to maintain. Индекс ремонтопригодности также обозначается какMI.

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

Чтобы рассчитать индекс ремонтопригодности изradon, выполните следующую команду:

$ radon mi cyclomatic_example.py -s
cyclomatic_example.py - A (87.42)

В этом результатеA - это оценка, которуюradon применил к числу87.42 на шкале. По этой шкалеA наиболее обслуживаемый, аF - наименее.

Использованиеwily для фиксации и отслеживания сложности ваших проектов

wily is an open-source software project для сбора показателей сложности кода, включая те, которые мы уже рассмотрели, такие как Halstead, Cyclomatic и LOC. wily интегрируется с Git и может автоматизировать сбор метрик по веткам и версиям Git.

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

Установкаwily

wily доступенon 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

Note: Пользователи Windows должны использовать для следующих примеров командную строку PowerShell вместо традиционной командной строки MS-DOS. Чтобы запустить интерфейс командной строки PowerShell, нажмитеWin[.kbd .key-r]#R## and type `+powershell` then [.keys] [.kbd .key-enter]Enter #.

Здесь вы увидите несколько папок для тестов, документации и конфигурации. Нас интересует только исходный код пакета Pythonrequests, который находится в папке с именемrequests.

Вызовите командуwily build из клонированного исходного кода и укажите имя папки исходного кода в качестве первого аргумента:

$ wily build requests

Это займет несколько минут для анализа, в зависимости от того, сколько ресурсов процессора имеет ваш компьютер:

Screenshot capture of Wily build command

Сбор данных о вашем проекте

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

  • Строки кода

  • Индекс ремонтопригодности

  • Цикломатическая Сложность

Это 3 показателя по умолчанию вwily. Чтобы увидеть эти показатели для определенного файла (например,requests/api.py), выполните следующую команду:

$ wily report requests/api.py

wily будет печатать табличный отчет по метрикам по умолчанию для каждого коммита Git в обратном порядке дат. Вы увидите самый последний коммит вверху и самый старый в нижней части:

пересмотр автор Date MI Строки кода Цикломатическая Сложность

f37daf2

Нейт Превитт

2019-01-13

100 (0,0)

158 (0)

9 (0)

6dd410f

Офек Лев

2019-01-13

100 (0,0)

158 (0)

9 (0)

5c1f72e

Нейт Превитт

2018-12-14

100 (0,0)

158 (0)

9 (0)

c4d7680

Матье Мой

2018-12-14

100 (0,0)

158 (0)

9 (0)

c452e3b

Нейт Превитт

2018-12-11

100 (0,0)

158 (0)

9 (0)

5a1e738

Нейт Превитт

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 и интерактивные диаграммы с таким же интерфейсом, что и команда отчета:

$ wily graph requests/sessions.py maintainability.mi

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

Screenshot capture of Wily graph command

Вы можете навести курсор мыши на определенные точки данных, и они будут отображать сообщение о коммите Git и данные.

Если вы хотите сохранить 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

В ответе вы увидите все измененные метрики, а также функции или классы, которые были изменены для цикломатической сложности:

Screenshot of the wily diff command

Команду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

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

Одна из старейших в мире подземных железных дорог, лондонское метро, ​​была основана в 1863 году с открытия линии метрополитена. Это были деревянные вагоны с газовой подсветкой, перевозимые паровозами. На открытии железной дороги это было не по назначению. 1900 год принес изобретение электрических железных дорог.

К 1908 году лондонское метро расширилось до 8 железных дорог. Во время Второй мировой войны станции лондонского метро были закрыты для поездов и использовались в качестве бомбоубежищ. Современное лондонское метро перевозит миллионы пассажиров в день на более чем 270 станциях:

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

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

В этом разделе вы узнаете, как безопасно проводить рефакторинг, используя тесты и инструменты. Вы также увидите, как использовать функцию рефакторинга вVisual Studio Code иPyCharm:

Refactoring Section Table of Contents

Избежание рисков с помощью рефакторинга: использование инструментов и проведение тестов

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

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

Если вы хотите узнать больше о тестировании на Python,Getting Started With Testing in Python - отличное место для начала.

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

Две наиболее распространенные задачи, которые вы будете выполнять при рефакторинге:

  • Переименование модулей, функций, классов и методов

  • Поиск использования функций, классов и методов, чтобы увидеть, где они вызываются

Вы можете просто сделать это вручную, используяsearch and replace, но это требует много времени и рискованно. Вместо этого есть несколько отличных инструментов для выполнения этих задач.

Использованиеrope для рефакторинга

rope - бесплатная утилита Python для рефакторинга кода Python. Он поставляется с набором APIextensive для рефакторинга и переименования компонентов в вашей кодовой базе Python.

rope можно использовать двумя способами:

  1. Используя плагин редактора, дляVisual Studio Code,Emacs илиVim

  2. Непосредственно путем написания скриптов для рефакторинга вашего приложения

Чтобы использовать веревку в качестве библиотеки, сначала установитеrope, выполнивpip:

$ pip install rope

Полезно работать сrope в REPL, чтобы вы могли исследовать проект и видеть изменения в реальном времени. Для начала импортируйте типProject и создайте его экземпляр с путем к проекту:

>>>

>>> from rope.base.project import Project

>>> proj = Project('requests')

Переменнаяproj теперь может выполнять ряд команд, таких какget_files иget_file, для получения определенного файла. Получите файлapi.py и назначьте его переменной с именемapi:

>>>

>>> [f.name for f in proj.get_files()]
['structures.py', 'status_codes.py', ...,'api.py', 'cookies.py']

>>> api = proj.get_file('api.py')

Если вы хотите переименовать этот файл, вы можете просто переименовать его в файловой системе. Однако любые другие файлы Python в вашем проекте, которые импортировали старое имя, теперь будут повреждены. Давайте переименуемapi.py вnew_api.py:

>>>

>>> from rope.refactor.rename import Rename

>>> change = Rename(proj, api).get_changes('new_api')

>>> proj.do(change)

Запустивgit status, вы увидите, чтоrope внес некоторые изменения в репозиторий:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   requests/__init__.py
    deleted:    requests/api.py

Untracked files:
  (use "git add ..." to include in what will be committed)

   requests/.ropeproject/
   requests/new_api.py

no changes added to commit (use "git add" and/or "git commit -a")

rope внес следующие три изменения:

  1. Удаленrequests/api.py и созданrequests/new_api.py

  2. Измененrequests/__init__.py для импорта изnew_api вместоapi

  3. Создана папка проекта с именем.ropeproject

Чтобы сбросить изменение, запуститеgit reset.

Естьhundreds of other refactorings, которые можно сделать с помощьюrope.

Использование кода Visual Studio для рефакторинга

Visual Studio Code открывает небольшое подмножество команд рефакторинга, доступных вrope через собственный пользовательский интерфейс.

Вы можете:

  1. Извлечение переменных из оператора

  2. Извлечение методов из блока кода

  3. Сортировать импорт в логическом порядке

Вот пример использования командыExtract methods из палитры команд:

Screenshot of Visual Studio Code refactoring

Использование PyCharm для рефакторинга

Если вы используете или планируете использовать PyCharm в качестве редактора Python, стоит обратить внимание на его мощные возможности рефакторинга.

Вы можете получить доступ ко всем ярлыкам рефакторинга с помощьюCtrl[.kbd .key-t]#T## command on Windows and macOS. The shortcut to access refactoring in Linux is [.keys]#[.kbd .key-control]##Ctrl##Shift[.kbd .key-alt]##Alt##[.kbd .key-t]#T #.

Поиск вызывающих абонентов и использование функций и классов

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

Чтобы получить доступ к этой функции, выберите метод, класс или переменную, щелкнув правой кнопкой мыши и выберитеFind Usages:

Finding usages in PyCharm

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

Использование инструментов рефакторинга PyCharm

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

  • Извлечение методов, переменных и констант из существующего кода

  • Извлечение абстрактных классов из существующих сигнатур классов, включая возможность указания абстрактных методов

  • Переименуйте практически что угодно, от переменной до метода, файла, класса или модуля

Вот пример переименования того же модуляapi.py, который вы переименовали ранее, используя модульrope, вnew_api.py:

How to rename methods in pycharm

Команда переименования контекстуализирована для пользовательского интерфейса, что делает рефакторинг быстрым и простым. Он автоматически обновил импорт в__init__.py с новым именем модуля.

Еще один полезный рефакторинг - командаChange Signature. Это может быть использовано для добавления, удаления или переименования аргументов функции или метода. Он будет искать способы использования и обновлять их для вас:

Changing method signatures in PyCharm

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

Резюме

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

Сложность анти-паттернов

Теперь, когда вы знаете, как можно измерить сложность, как ее измерить и как реорганизовать код, пришло время изучить 5 распространенных анти-паттернов, которые делают код более сложным, чем нужно:

Anti-Patterns Table of Contents

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

1. Функции, которые должны быть объектами

Python поддерживаетprocedural programming с использованием функций, а такжеinheritable classes. Оба очень мощные и должны применяться к различным проблемам.

Возьмите этот пример модуля для работы с изображениями. Логика в функциях была удалена для краткости:

# imagelib.py

def load_image(path):
    with open(path, "rb") as file:
        fb = file.load()
    image = img_lib.parse(fb)
    return image

def crop_image(image, width, height):
    ...
    return image

def get_image_thumbnail(image, resolution=100):
    ...
    return image

Есть несколько проблем с этим дизайном:

  1. Неясно, изменяют лиcrop_image() иget_image_thumbnail() исходную переменнуюimage или создают новые изображения. Если вы хотите загрузить изображение, а затем создать как обрезанное, так и миниатюрное изображение, вам придется сначала скопировать экземпляр? Вы можете прочитать исходный код в функциях, но вы не можете рассчитывать на то, что это сделает каждый разработчик.

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

Вот как может выглядеть вызывающий код:

from imagelib import load_image, crop_image, get_image_thumbnail

image = load_image('~/face.jpg')
image = crop_image(image, 400, 500)
thumb = get_image_thumbnail(image)

Вот некоторые признаки кода, использующего функции, которые могут быть преобразованы в классы:

  • Подобные аргументы в разных функциях

  • Большее число Холстедаh2unique operands

  • Сочетание изменчивых и неизменных функций

  • Функции распределены по нескольким файлам Python

Вот переработанная версия этих 3 функций, где происходит следующее:

  • .__init__() заменяетload_image().

  • crop() становится методом класса.

  • get_image_thumbnail() становится собственностью.

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

# imagelib.py

class Image(object):
    thumbnail_resolution = 100
    def __init__(self, path):
        ...

    def crop(self, width, height):
        ...

    @property
    def thumbnail(self):
        ...
        return thumb

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

Вот как будет выглядеть измененный пример:

from imagelib import Image

image = Image('~/face.jpg')
image.crop(400, 500)
thumb = image.thumbnail

В полученном коде мы решили исходные проблемы:

  • Понятно, чтоthumbnail возвращает миниатюру, поскольку это свойство и не изменяет экземпляр.

  • Код больше не требует создания новых переменных для операции обрезки.

2. Объекты, которые должны быть функциями

Иногда верно обратное. Существует объектно-ориентированный код, который лучше подходит для одной или двух простых функций.

Вот некоторые признаки неправильного использования классов:

  • Классы с 1 методом (кроме.__init__())

  • Классы, которые содержат только статические методы

Возьмите этот пример класса аутентификации:

# authenticate.py

class Authenticator(object):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        ...
        return result

Было бы разумнее просто иметь простую функцию с именемauthenticate(), которая принимает в качестве аргументовusername иpassword:

# authenticate.py

def authenticate(username, password):
    ...
    return result

Вам не нужно вручную искать классы, соответствующие этим критериям:pylint поставляется с правилом, согласно которому классы должны иметь как минимум 2 открытых метода. Чтобы узнать больше о PyLint и других инструментах для обеспечения качества кода, вы можете проверитьPython Code Quality.

Чтобы установитьpylint, выполните в консоли следующую команду:

$ pip install pylint

pylint принимает ряд необязательных аргументов, а затем путь к одному или нескольким файлам и папкам. Если вы запуститеpylint с настройками по умолчанию, он даст много результатов, посколькуpylint имеет огромное количество правил. Вместо этого вы можете запустить определенные правила. Идентификатор правилаtoo-few-public-methods -R0903. Вы можете посмотреть это наdocumentation website:

$ pylint --disable=all --enable=R0903 requests
************* Module requests.auth
requests/auth.py:72:0: R0903: Too few public methods (1/2) (too-few-public-methods)
requests/auth.py:100:0: R0903: Too few public methods (1/2) (too-few-public-methods)
************* Module requests.models
requests/models.py:60:0: R0903: Too few public methods (1/2) (too-few-public-methods)

-----------------------------------
Your code has been rated at 9.99/10

Этот вывод сообщает нам, чтоauth.py содержит 2 класса, у которых есть только 1 открытый метод. Эти классы в строках 72 и 100. Также есть класс в строке 60models.py только с 1 общедоступным методом.

3. Преобразование «треугольного» кода в плоский код

Если бы вам пришлось уменьшить исходный код и наклонить голову на 90 градусов вправо, станет ли пробел плоским, как Голландия, или горным, как Гималаи? Горный код является признаком того, что ваш код содержит много вложений.

Вот один из принциповZen of Python:

«Квартира лучше вложенной»

- Тим Питерс, дзен питона

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

Это признаки сильно вложенного кода:

  • Высокая цикломатическая сложность из-за количества ветвей кода

  • Низкий индекс ремонтопригодности из-за высокой цикломатической сложности по сравнению с количеством строк кода

Возьмем этот пример, который рассматривает аргументdata для строк, соответствующих словуerror. Сначала он проверяет, является ли аргументdata списком. Затем он перебирает все и проверяет, является ли элемент строкой. Если это строка и значение"error", то возвращаетсяTrue. В противном случае возвращаетсяFalse:

def contains_errors(data):
    if isinstance(data, list):
        for item in data:
            if isinstance(item, str):
                if item == "error":
                    return True
    return False

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

Вместо этого мы можем провести рефакторинг этой функции, «вернув раньше», чтобы удалить уровень вложенности и вернутьFalse, если значениеdata не указано в списке. Затем используйте.count() в объекте списка для подсчета экземпляров"error". Тогда возвращаемое значение является оценкой того, что.count() больше нуля:

def contains_errors(data):
    if not isinstance(data, list):
        return False
    return data.count("error") > 0

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

results = []
for item in iterable:
    if item == match:
        results.append(item)

Этот код можно заменить на более быстрое и эффективное понимание списка.

Реорганизуйте последний пример в понимание списка и выражениеif:

results = [item for item in iterable if item == match]

Этот новый пример меньше, имеет меньшую сложность и более производительный.

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

Itertools также содержит функции для фильтрации данных, напримерfilterfalse(). Чтобы узнать больше об Itertools, посетитеItertools in Python 3, By Example.

4. Обработка сложных словарей с помощью инструментов запросов

Одним из наиболее мощных и широко используемых типов ядра Python является словарь. Это быстро, эффективно, масштабируемо и очень гибко.

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

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

Возьмите этот пример данных, пример линий метро Токио, которые вы видели ранее:

data = {
 "network": {
  "lines": [
    {
     "name.en": "Ginza",
     "name.jp": "銀座線",
     "color": "orange",
     "number": 3,
     "sign": "G"
    },
    {
     "name.en": "Marunouchi",
     "name.jp": "丸ノ内線",
     "color": "red",
     "number": 4,
     "sign": "M"
    }
  ]
 }
}

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

def find_line_by_number(data, number):
    matches = [line for line in data if line['number'] == number]
    if len(matches) > 0:
        return matches[0]
    else:
        raise ValueError(f"Line {number} does not exist.")

Даже если сама функция небольшая, вызов функции неоправданно сложен, потому что данные так вложены:

>>>

>>> find_line_by_number(data["network"]["lines"], 3)

Существуют сторонние инструменты для запроса словарей в Python. Некоторые из самых популярных - этоJMESPath,glom,asq иflupy.

JMESPath может помочь с нашей сетью поездов. JMESPath - это язык запросов, разработанный для JSON, с плагином, доступным для Python, который работает со словарями Python. Чтобы установить JMESPath, сделайте следующее:

$ pip install jmespath

Затем откройте Python REPL, чтобы изучить JMESPath API, скопировав его в словарьdata. Для начала импортируйтеjmespath и вызовитеsearch() со строкой запроса в качестве первого аргумента и данными в качестве второго. Строка запроса"network.lines" означает возвратdata['network']['lines']:

>>>

>>> import jmespath

>>> jmespath.search("network.lines", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線',
  'color': 'orange', 'number': 3, 'sign': 'G'},
 {'name.en': 'Marunouchi', 'name.jp': '丸ノ内線',
  'color': 'red', 'number': 4, 'sign': 'M'}]

При работе со списками вы можете использовать квадратные скобки и предоставить запрос внутри. Запрос «все» - это просто*. Затем вы можете добавить имя атрибута внутри каждого соответствующего элемента для возврата. Если вы хотите получить номер строки для каждой строки, вы можете сделать это:

>>>

>>> jmespath.search("network.lines[*].number", data)
[3, 4]

Вы можете предоставить более сложные запросы, например== или<. Синтаксис немного необычен для разработчиков Python, поэтому держитеdocumentation под рукой для справки.

Если мы хотим найти строку с номером3, это можно сделать одним запросом:

>>>

>>> jmespath.search("network.lines[?number==`3`]", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線', 'color': 'orange', 'number': 3, 'sign': 'G'}]

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

>>>

>>> jmespath.search("network.lines[?number==`3`].color", data)
['orange']

JMESPath может использоваться для сокращения и упрощения кода, который запрашивает и ищет в сложных словарях.

5. Использованиеattrs иdataclasses для сокращения кода

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

Некоторые другие методы требуют знания стандартной библиотеки и некоторых сторонних библиотек.

Что такое Boilerplate?

Код Boilerplate - это код, который должен использоваться во многих местах практически без изменений.

Если взять в качестве примера нашу сеть поездов, если бы мы преобразовали ее в типы с использованием классов Python и подсказок типов Python 3, это могло бы выглядеть примерно так:

from typing import List

class Line(object):
    def __init__(self, name_en: str, name_jp: str, color: str, number: int, sign: str):
        self.name_en = name_en
        self.name_jp = name_jp
        self.color = color
        self.number = number
        self.sign = sign

    def __repr__(self):
        return f""

    def __str__(self):
        return f"The {self.name_en} line"

class Network(object):
    def __init__(self, lines: List[Line]):
        self._lines = lines

    @property
    def lines(self) -> List[Line]:
        return self._lines

Теперь вы можете добавить другие магические методы, например.__eq__(). Этот код является стандартным. Здесь нет бизнес-логики или какой-либо другой функциональности: мы просто копируем данные из одного места в другое.

Случай дляdataclasses

Введенный в стандартную библиотеку в Python 3.7, с пакетом backport для Python 3.6 для PyPI, модуль dataclasses может помочь удалить много шаблонного кода для этих типов классов, где вы просто храните данные.

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

from dataclasses import dataclass

@dataclass
class Line(object):
    name_en: str
    name_jp: str
    color: str
    number: int
    sign: str

Затем вы можете создать экземпляр типаLine с теми же аргументами, что и раньше, с теми же полями, и даже реализованы.__str__(),.__repr__() и.__eq__():

>>>

>>> line = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line.color
red

>>> line2 = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line == line2
True

Классы данных - отличный способ сократить код с помощью одного импорта, который уже доступен в стандартной библиотеке. Для полного прохождения вы можете проверитьThe Ultimate Guide to Data Classes in Python 3.7.

Некоторые варианты использованияattrs

attrs - это сторонний пакет, который существует намного дольше, чем классы данных. attrs имеет гораздо больше функций и доступен на Python 2.7 и 3.4+.

Если вы используете Python 3.5 или ниже,attrs - отличная альтернативаdataclasses. Кроме того, он предоставляет гораздо больше возможностей.

Пример эквивалентных классов данных вattrs будет выглядеть аналогичным образом. Вместо использования аннотаций типов атрибутам класса присваивается значение изattrib(). Это может принимать дополнительные аргументы, такие как значения по умолчанию и обратные вызовы для проверки ввода:

from attr import attrs, attrib

@attrs
class Line(object):
    name_en = attrib()
    name_jp = attrib()
    color = attrib()
    number = attrib()
    sign = attrib()

attrs может быть полезным пакетом для удаления шаблонного кода и проверки ввода для классов данных.

Заключение

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

  • Начните с создания базовой линии вашего проекта с помощью такого инструмента, какwily.

  • Посмотрите на некоторые показатели и начните с модуля с самым низким индексом ремонтопригодности.

  • Выполните рефакторинг этого модуля, используя безопасность, предоставленную в тестах, и знания таких инструментов, как PyCharm иrope.

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