Википедия гласит:
Рефакторинг (англ. refactoring), или переработка кода, равносильное преобразование алгоритмов - процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы. В основе рефакторинга лежит последовательность небольших эквивалентных (то есть сохраняющих поведение) преобразований, которая в последствии может привести к существенной перестройке программы и улучшению её согласованности и чёткости.
Из определения видно, что рефакторинг кода - это попытка сделать код программы лучше. Улучшение кода может означать разные вещи, в зависимости от контекста:
Каждый может извлечь пользу из обучения рефакторингу, так как происходит тренировка ряда навыков, например, способность читать код и действительно понимать его, распознавания образов, критическое мышление и т.д.
Одно из главных условий рефакторинга фрагмента кода, это необходимость понимать, что код делает и как он это делает. Если изменить часть кода, не понимая его, то шансы сломать программу возрастают.
Одна из вещей, на которые необходимо обратить внимание при рефакторинге - это избыточность и повторяемость. Такие вещи очень просто заметить, т.к. в коде будут идентичные строки кода (повторения). Найти структурные сходства между различными частями кода сложнее, чем найти идентичные строки. Но после того как код становится более понятным, сделать это становится намного проще (тренировка навыков распознавания образов).
Читая свой, ранее написанный код, вы несомненно найдете фрагменты, которые находятся, например в не том файле (если это пакет Python). Кусок кода, который находится в неправильной функции или даже кусок кода, который выглядит так, как будто его можно и нужно удалить (критическое мышление).
Рефакторинг также можно охарактеризовать как бесконечный цикл. По мере приобретения опыта, разработчик узнает что-то новое, кроме того, технологии, которые использовались при написании кода год назад, вероятно, претерпели изменения. Это означает, что ранее написанный код естественным образом переходит в состояние необходимости рефакторинга.
if/else
в одну строку;Задача: напишите функцию, которая меняет регистр букв передаваемого ей слова:
Фрагмент кода, который сразу приходит на ум:
def textfunc(a): empty=[] for i in range(len(a)): if i%2==0: empty.append(a[i].upper()) else: empty.append(a[i].lower()) return "".join(empty)
Самый первый шаг, который можно сделать для написания элегантного кода - это запуск инструмента для проверки кода Python на соответствие некоторым соглашениям о стилях в PEP 8 например pycodestyle
(дает рекомендации по исправлению). Также есть инструменты автоматического исправления кода. Например, если использовать сторонний модуль black
, то с самого начала можно исправить многие несоответствия и проблемы со стилем.
def textfunc(a): empty = [] for i in range(len(a)): if i % 2 == 0: empty.append(a[i].upper()) else: empty.append(a[i].lower()) return "".join(empty)
Разница этого кода заключается в пробелах в empty = []
и if i % 2 == 0:
. Если код следует руководству PEP 8 Python, то он будет более читаемым для себя и для других. Также необходимо знать, что некоторые команды разработчиков частично игнорируют руководство по стилю PEP 8, т. к. у них разработаны собственные стандарты по оформлению кода (в основном это касается правил написания имен функций, классов и их методов, а также количества символов для переноса строки с кодом).
black
.Модуль black
- форматировщик кода Python, который избавляет от мелочей ручного форматирования. Он дает скорость, детерминизм и свободу от pycodestyle
, который очень придирчив к форматированию. Модуль black
ускоряет проверку кода, производя наименьшие возможные различия.
Модуль black
можно установить, запустив pip install black
. Для запуска требуется Python 3.7+. Если необходимо отформатировать ноутбуки Jupyter, то установите его с помощью pip install 'black[jupyter]'
.
Чтобы запустить форматирование кода с разумными настройками по умолчанию, нужно выполнить команду:
$ black {source_file_or_directory}
Если запуск black
как скрипта не работает, можно запустить его как модуль Python:
$ python3 -m black {source_file_or_directory}
Все настройки/параметры командной строки модуля black
можно отобразить, выполнив команду терминала $ black --help
.
Имена очень важны, и правильное наименование функций имеет решающее значение. Хорошие имена сразу дают понять, что делает код функции класса или метода, а плохие имена заставляют часами анализировать простой код.
Имена должны отражать назначение или очень важное свойство того, к чему они относятся. Это противоположно использованию очень общих имен, таких как textfunc
для функции или num
для числа.
Заметным исключением является, например, использование i
в циклах for/in
, хотя предпочтительнее использовать немного более подробное имя, например idx
.
Глядя на код, который есть в настоящее время, можно выделить три имени, которые можно было бы улучшить.
def text_decoration(text): letters = [] for idx in range(len(text)): if idx % 2 == 0: letters.append(text[idx].upper()) else: letters.append(text[idx].lower()) return "".join(letters)
Изменения, которые сделаны:
myfunc
переименовываем в text_decoration
;empty
переименовываем в letters
a
в text
;i
в idx
(необязательно).По началу, имя для списка empty = []
кажется довольно неплохим. Но после того, как инициализируется пустой список, программа начинает его заполнять, поэтому имя не отражает свойство объекта, которое сохраняется на протяжении всей программы.
В Python есть несколько действительно хороших возможностей для работы с циклами for
, и один из инструментов, который предоставляется - это встроенная функция enumerate
. Это инструмент, к которому нужно всегда обращаться, когда цикл for
работает с индексами и данными одновременно.
В функции text_decoration()
как раз нужны индексы и данные, так как нужен индекс для определения выполняемой операции, а затем нужны данные (фактическая буква), чтобы изменить ее регистр.
def text_decoration(text): letters = [] for idx, char in enumerate(text): if idx % 2 == 0: letters.append(char.upper()) else: letters.append(char.lower()) return "".join(letters)
В измененном коде функции убрали явное индексирование (text[idx]
), сократив тем самым одну операцию, а также более четко выражен замысел: применение enumerate
в цикле всегда означает, что "в этом цикле нужны как индексы, так и данные”.
В Python отступ указывает на вложенность кода, что указывает на зависимость. Если строка кода вложена в цикл for
, то это означает, что она зависит от цикла for
. Если он дополнительно вложен в оператор if/else
, это означает, что он применяется только при выполнении определенных условий. Если он дополнительно вложен в оператор try/except
, то можно ожидать, что он вызовет ошибку и т. д.
Вложенность кода означает, что нужно отслеживать множество контекстов в голове. Чтобы проще было следить за контекстом, нужно стараться как можно меньше писать вложенный код. Необходимо вкладывать только те фрагменты кода, которые абсолютно необходимы для этого. Для циклов for
это обычно вещи, которые зависят от переменных итератора между for
и in
, а для операторов if/else
это фрагменты кода, которые уникальны для каждого оператора.
Тело функции text_decoration()
имеет такой код:
if idx % 2 == 0: letters.append(char.upper()) else: letters.append(char.lower())
Обратите внимание, что код вызывает letter.append()
независимо от ветки, в которой находится. Это делает менее очевидным, то, что меняется от одной ветки к другой, так как методы str.upper()
и str.lower()
занимают одинаковое количество символов, и следовательно выровнены.
Становиться очевидным необходимость вынести операцию добавления символов letter.append()
из веток условия if/else
. В итоге, результирующий код функции меняется на следующий:
def text_decoration(text): letters = [] for idx, char in enumerate(text): if idx % 2 == 0: capitalised = char.upper() else: capitalised = char.lower() letters.append(capitalised) return "".join(letters)
Можно конечно много рассуждать на тему, что код стал длиннее, а не короче. Но принимая во внимание один из пунктов философии языка Python ("Explicit is better than implicit") иногда более качественный код занимает больше места.
if/else
в одну строку.Вынесение letters.append()
за пределы if/else
делает совершенно очевидным, что оператор if
предназначен только для того, чтобы решить, когда нужно использовать строчные или заглавные буквы. Это открывает дверь для еще одного упрощения, которое придет в виде условного выражения в одну строку. Используя однострочник if/else
, перепишем условие как:
capitalised = char.upper() if idx % 2 == 0 else char.lower()
По большому счету, промежуточная переменная capitalised
не нужна, так как тело цикла можно записать как:
def text_decoration(text): letters = [] for idx, char in enumerate(text): letters.append(char.upper() if idx % 2 == 0 else char.lower()) return "".join(letters)
Следующий шаг касается упрощения условия оператора if
. В Python есть замечательная вещь, которая позволяет интерпретировать многие объекты как булевы значения, даже если они сами не являются логическими значениями. Это часто называют истинность объекта в Python.
В случае с условием if idx % 2 == 0:...
важно то, что число 0 рассматривается как False
, а любое другое целое число рассматривается как True
. Следовательно, условие можно записать как if idx % 2:...
. Теперь, если индекс idx
- четный, то результат idx % 2
будет РАВЕН 0 (значит False
), следовательно букву необходимо делать заглавной. Принимая во внимание это утверждение условие выражение в одну строку примет вид:
char.lower() if idx % 2 else char.upper()
Перепишем функцию text_decoration()
используя истинность объектов в Python:
def text_decoration(text): letters = [] for idx, char in enumerate(text): letters.append(char.lower() if idx % 2 else char.upper()) return "".join(letters)
Еще одну вещь, которую можно научиться замечать - это когда строится список, последовательно вызывая для него list.append()
. В этом случае, необходимо искать возможность использовать для этого выражение-генератор списка. При правильном использовании, генератор списка позволяет инициализировать переменную списка letters
с правильным содержимым с самого начала, а не инициализировать переменную, чтобы сразу изменить ее.
Используя выражение-генератор списка, можно переписать цикл следующим образом:
def text_decoration(text): letters = [char.lower() if idx % 2 else char.upper() for idx, char in enumerate(text)] return "".join(letters)
Проблема со списком выше заключается в том, что теперь существует очень длинная строка кода. По возможности следует избегать длинных строк, т.к. они затрудняют чтение и работу с кодом, когда он находится рядом, например, с отладчиком и т.д. Горизонтальной прокрутки в коде следует избегать любой ценой.
Есть несколько способов, что бы исправить длинный код выражения-генератора списка.
Первый основывается на том, что имена переменных используемые генератором списка живут только внутри него и играют очень специфическую роль. Из-за этого, если структура происходящего достаточно ясна, то можно использовать более короткие имена переменных внутри генератора списка:
def text_decoration(text): letters = [c.lower() if i % 2 else c.upper() for i, c in enumerate(text)] return "".join(letters)
Обратите внимание, что целевая переменная имеет правильное имя (letters
), как и переменная, по которой итерируемся (text
).
Второй способ основывается на синтаксисе Python: то, что внутри любых скобок - можно переносить. Так что, если предпочтительнее оставить длинные имена, то генератор списка можно записать следующим образом:
def text_decoration(text): letters = [ char.lower() if idx % 2 else char.upper() for idx, char in enumerate(text) ] return "".join(letters)
Вспомогательные переменные не всегда нужны. В данном конкретном случае можно просто избавиться от вспомогательной переменной letters
и напрямую вызвать метод строки str.join()
:
def text_decoration(text): return "".join([c.lower() if i % 2 else c.upper() for i, c in enumerate(text)])
или
def text_decoration(text): return "".join([ char.lower() if idx % 2 else char.upper() for idx, char in enumerate(text) ])
Есть еще одна последняя вещь, которую можно сделать, и она связана с тем, как можно избавиться от скобок []
в генераторе списка. Можно буквально удалить их, так что в итоге получим следующее:
def text_decoration(text): return "".join(c.lower() if i % 2 else c.upper() for i, c in enumerate(text))
или
def text_decoration(text): return "".join( char.lower() if idx % 2 else char.upper() for idx, char in enumerate(text) )
f-строк
вместо форматирования строк str.format()
там где это возможно (f-строки не могут использоваться как шаблоны строк).:=
в условиях if/else
и циклах с условием while
.