Вторая статья про рефакторинг в Python. Первую статью можно почитать здесь.
Рефакторинг — это улучшения кода приложения (или архитектуры) за счет внесения внутренних изменений, без изменения внешнего функционала. Этими улучшениями могут быть увеличение стабильности, производительности или уменьшение сложности.
Одна из старейших в мире подземных железных дорог, лондонское метро, было основано в 1863 году. Это было метро с деревянные вагонами с газовой подсветкой, перевозимые паровозами. И только в 1900 году появились электрические поезда.
К 1908 году лондонское метро расширилось до 8 железных дорог. Во время Второй мировой войны станции лондонского метро были закрыты для поездов и использовались в качестве бомбоубежищ. Современное лондонское метро перевозит миллионы пассажиров в день на более чем 270 станциях:
Почти невозможно написать идеальный код с первого раза, и требования часто меняются. Если бы вы попросили первоначальных создателей железной дороги спроектировать сеть, рассчитанную на 10 миллионов пассажиров в день в 2020 году, они бы не спроектировали сеть, которая в итоге существует сегодня, они бы могли бы сделать это гораздо лучше, но так не бывает.
Вместо этого железная дорога претерпела серию непрерывных изменений, чтобы улучшить ее работу, дизайн и планировку в соответствии с изменениями в городе. Это был своего рода рефакторинг.
В этой статье вы узнаете, как безопасно проводить рефакторинг, используя тесты и современные инструменты. Вы также увидите, как использовать функцию рефакторинга в Visual Studio Code и PyCharm:
Если смысл рефакторинга состоит в том, чтобы улучшить внутреннюю часть приложения, не влияя на внешние, как узнать, что внешние элементы не изменились?
Прежде чем приступить к серьезному рефакторингу большого проекту, вы должны убедиться, что у вас есть надежный набор тестов для вашего приложения. В идеале, этот набор тестов должен быть в основном автоматизированным, чтобы при внесении изменений вы видели влияние на пользователя и быстро устраняли егов случае необходимости.
Если вы хотите узнать больше о тестировании в Python, почитайте эту статью Getting Started With Testing in Python.
Не существует идеального количества тестов для вашего приложения. Но чем надежнее и тщательнее будет создан набор тестов, тем более агрессивно вы можете проводить рефакторинг своего кода.
Две наиболее распространенные задачи, которые вы будете выполнять при рефакторинге:
Вы можете делать это вручную, используя поиск и замену, но это отнимет много времени и будет достаточно рискованно. Вместо этого есть несколько отличных инструментов для выполнения этих задач.
rope — бесплатная утилита для рефакторинга кода Python. Эта утилита поставляется с обширным набором API для рефакторинга и переименования компонентов.
rope
можно использовать двумя способами:
Чтобы использовать rope в качестве библиотеки, сначала установите 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 через rope:
>>> 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 <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: requests/__init__.py deleted: requests/api.py Untracked files: (use "git add <file>..." 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, следующие:
requests/api.py
и создан requests/new_api.py
requests/__init__.py
, в нем добавлен импорт new_api
и удален импорт api
.ropeproject
Что бы сбросить все изменения вы можете запустить git reset
.
Обо всем что можно сделать с rope можно почитать здесь.
Visual Studio Code содержит небольшое подмножество команд рефакторинга, доступных в отдельном пользовательском интерфейсе.
С помощью его вы можете:
Вот пример использования команды «Извлечь методы» из палитры команд:
Если вы используете или планируете использовать PyCharm в качестве редактора Python, стоит обратить внимание на его мощные возможности рефакторинга.
Вы можете получить доступ ко всем функциям рефакторинга с помощью команды Ctrl + T в Windows и macOS. Ярлык для доступа к рефакторингу в Linux — Ctrl + Shift + Alt + T.
Перед тем, как удалить метод или класс или изменить его поведение, вам необходимо знать, какой код зависит от него. PyCharm может искать все способы использования метода, функции или класса в вашем проекте.
Чтобы получить доступ к этой функции, выберите метод, класс или переменную, щелкнув правой кнопкой мыши и выберите «Найти использование»(Find Usages):
Весь код, который использует ваши критерии поиска, показан на панели внизу. Вы можете дважды щелкнуть по любому элементу, чтобы перейти непосредственно к рассматриваемой строке.
Некоторые из других команд рефакторинга включают в себя возможность:
Вот пример переименования того же модуля api.py, который вы переименовали ранее, с помощью rope в new_api.py:
Команда переименования (rename) контекстуализирована для пользовательского интерфейса, что делает рефакторинг быстрым и простым.
Другим полезным рефакторингом является команда Изменить подпись (Change Signature). Она может быть использовано для добавления, удаления или переименования аргументов функции или метода. Эта команда ищет все случаи использования и обновляет их для вас:
Вы можете установить значения по умолчанию, а также решить, как рефакторинг должен обрабатывать новые аргументы.
Рефакторинг — важный навык для любого разработчика. Инструменты и IDE уже оснащены мощными функциями рефакторинга, позволяющими быстро вносить изменения.
Теперь, когда вы знаете, как можно измерить сложность, как ее измерить и как реорганизовать ваш код, пришло время изучить 5 распространенных анти-шаблонов, которые делают код более сложным, чем нужно:
Python поддерживает процедурное программирование с использованием функций, а также программирование с помощью ООП. Оба подхода самодостаточны и должны применяться к различным проблемам.
Возьмем для примера модуль для работы с изображениями. Логика в функциях была удалена для краткости:
# 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
Есть несколько проблем с этим дизайном:
Вот как может выглядеть вызывающий код:
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)
Некоторые признаки кода, использующего функции, который может быть преобразованы в классы:
Переработанная версия этих трех функций, где происходит следующее:
.__init__()
заменяет load_image()
.crop()
становится методом класса.get_image_thumbnail()
становится свойством.Переменная thumbnail_resolution стало свойством класса, теперь его можно изменить глобально или в конкретном случае:
# 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
В полученном коде мы решили исходные проблемы:
Иногда верно обратное. Существует объектно-ориентированный код, который лучше подходит для одной или двух простых функций.
Вот несколько признаков неправильного использования классов:
Возьмем этот пример класса аутентификации:
# authenticate.py class Authenticator(object): def __init__(self, username, password): self.username = username self.password = password def authenticate(self): ... return result
Было бы разумнее иметь простую функцию authenticate(), которая принимает имя пользователя и пароль в качестве аргументов:
# authenticate.py def authenticate(username, password): ... return result
Вам не нужно сидеть и искать классы, которые соответствуют этим критериям вручную: в pylint есть правило, согласно которому классы должны иметь как минимум 2 открытых метода. Чтобы узнать больше о PyLint и других инструментах проверки качества кода, вы можете почитать здесь Python Code Quality.
Чтобы установить pylint, введите в консоли следующую команду:
$ pip install pylint
pylint принимает несколько необязательных аргументов, а затем путь к одному или нескольким файлам и папкам. Если вы запустите pylint с его настройками по умолчанию, он выдаст много выходных данных, поскольку pylint по умолчанию содержит огромное количество правил. Вместо этого вы можете запустить проверку на определенные правила. Идентификатор правила «слишком мало открытых методов» — R0903. Вы можете почитать об этом больше на сайте документации:
$ 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. Существует также класс в строке 60 models.py только с одним открытым методом.
Если бы вам пришлось уменьшить исходный код и наклонить голову на 90 градусов вправо, станет ли пробел плоским, как Голландия, или горным, как Гималаи? Горный код является признаком того, что ваш код содержит много вложений.
Один из принципов в дзен Python (Zen of Python) гласит:
Плоский код лучше вложенного “Flat is better than nested”
— Tim Peters, 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)
Этот код можно заменить более быстрым и эффективным генератором списка (list comprehension).
Преобразуйте последний пример в список с оператором if:
results = [item for item in iterable if item == match]
Этот новый пример меньше, имеет меньшую сложность и более производительный.
Если ваши данные не являются списком, вы можете использовать пакет itertools в стандартной библиотеке, которая содержит функции для создания итераторов из структур данных. Вы можете использовать его для объединения итераций, отображения структур, циклического повторения или повторения существующих итераций.
Itertools также содержит функции для фильтрации данных, такие как filterfalse()
. Чтобы узнать больше об Itertools, ознакомьтесь с Itertools in Python 3, By Example.
Одним из самых мощных и широко используемых типов данных 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, чтобы изучить API JMESPath, скопировав его в словарь данных. Для начала импортируйте 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, поэтому держите документацию под рукой.
Если мы хотим найти строку с номером 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 можно использовать для сокращения и упрощения кода, который запрашивает и ищет в сложных словарях.
attrs
и dataclasses
для уменьшение кодаДругая цель при рефакторинге состоит в том, чтобы просто уменьшить объем кода в кодовой базе при достижении того же поведения.
Код 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"<Line {self.name_en} color='{self.color}' number={self.number} sign='{self.sign}'>" 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, модуль dataclasses может помочь удалить множество шаблонов для типов классов, где вы просто храните данные.
Чтобы преобразовать приведенный выше класс Line в dataclass, преобразуйте все поля в атрибуты класса и убедитесь, что они имеют аннотации типов:
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
Dataclasses — отличный способ уменьшить код с помощью одного импорта, который уже доступен в стандартной библиотеке. Для полного ознакомления вы можете обратиться к The Ultimate Guide to Data Classes in Python 3.7.
attrs
attrs — это сторонний пакет, который существует намного дольше, чем dataclass. attrs и обладает гораздо большей функциональностью а так же доступен на Python 2.7 и 3.4+.
Если вы используете Python 3.5 или ниже, attrs — отличная альтернатива dataclass. Кроме того, он предоставляет гораздо больше возможностей.
Эквивалентный пример классов данных в attrs будет выглядеть аналогично. Вместо использования аннотаций типов атрибутам класса присваивается значение из attrib(). Это может принимать дополнительные аргументы, такие как значения по умолчанию и обратные вызовы для проверки ввода:
from attr import attrs, attrib @attrs class Line(object): name_en = attrib() name_jp = attrib() color = attrib() number = attrib() sign = attrib()
attrs может быть полезным пакетом для удаления стандартного (boilerplate) кода и проверки правильности ввода для классов данных.
Теперь, когда вы узнали, как распознавать и обрабатывать сложный код, вспомните шаги, которые вы можете предпринять, чтобы упростить рефакторинг и улучшить управление вашим приложением:
Выполнив эти шаги и рекомендации из этой статьи, вы сможете делать и другие интересные вещи в своем приложении, такие как добавление новых функций и повышение производительности.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…