Распутываем сильно вложенный код Python
29 марта 2023 г.Считается, что сильно вложенный код трудно читать и анализировать. Это связано с тем, что такой код требует больше умственных усилий, чтобы понять, что он делает. Чем больше вложенности кода, тем выше его цикломатическая сложность, что затрудняет его понимание и тестирование. Вот почему глубоко вложенный код часто рассматривается как анти-шаблон, которого следует избегать.
Одним из методов рефакторинга для уменьшения вложенности структур является использование защитных предложений, также называемых защитные операторы. Это фрагменты кода в верхней части функции, которые служат следующим целям:
* проверка переданных параметров на пригодность * проверка состояния объекта для принятия решения о продолжении * отлов непредвиденных ситуаций перед выполнением основного тела
Таким образом мы можем уменьшить уровень отступов и значительно упростить структуру кода.
Дополнительный уровень вложенности из-за проверки передаваемых параметров
Мы рассмотрим применение защитных предложений с точки зрения проверки передаваемых параметров. Давайте рассмотрим два примера из практики, когда тела функций помещались в один оператор if. Я немного изменил примеры, но сохранил основную идею.
def get_characters(file_path: Path) -> List[str]:
result = []
if file_path.exists():
with open(file_path, 'r') as file:
for line in file:
if not is_character_line(line):
result.append(line)
return result
В верхней части функции мы проверяем, существует ли файл. Если это так, мы переходим к оператору if, где выполняется основное тело функции. Мы читаем файл построчно, добавляя соответствующие строки в список результатов. В конце функции возвращаем результат. Мы можем упростить функцию и избавиться от излишней вложенности, введя в ее начало защитное предложение.
Давайте посмотрим на другой пример, где первая строка вводит вложенный блок для всей функции.
@dataclass
class AtmMachine:
code: str
name: str
city: str
def print_atm_statistics(atms: List[AtmMachine]) -> None:
if len(atms) > 0:
basic_atm = atms[0]
print_atms_in_city(basic_atm.city)
process_atms_codes(atms)
process_atms_names(atms)
print_summary(atms)
Код даже кажется странным, потому что вся функция помещается внутри оператора if. Класс данных определяет банкомат, который имеет три параметра, описывающих его. Функция print_atm_statistics
получает в качестве параметра список банкоматов для их обработки. В случае допустимого параметра вся обработка выполняется внутри одного условного оператора.
Помещение всей функции в одно условное выражение — частый паттерн, встречающийся в моей работе. Функции начинаются с проверки условного представления параметров.
Представляем ограничительную оговорку
Мы могли бы улучшить примеры, добавив защитное предложение, которое будет проверять наличие неподходящих параметров в верхней части функции, уменьшив один избыточный уровень вложенности.
Первый пример преобразуется в:
def get_characters(file_path: Path) -> List[str]:
if not file_path.exists():
return []
result = []
with open(file_path, 'r') as file:
for line in file:
if not is_character_line(line):
result.append(line)
return result
Введение сторожевого предложения позволило нам убрать один уровень отступа и, таким образом, улучшить читабельность. Разумно сразу вернуться, если какой-то из параметров по какой-то причине не устраивает. В этом случае, если файл не существует, мы мгновенно возвращаемся, что означает, что нам нечего обрабатывать.
Что касается второго примера с банкоматами:
def print_atm_statistics(atms: List[AtmMachine]) -> None:
if not len(atms):
return
basic_atm = atms[0]
print_atms_in_city(basic_atm.city)
process_atms_codes(atms)
process_atms_names(atms)
print_summary(atms)
Защита также помогла убрать лишний уровень вложенности внутри функции. Мы инвертировали условное выражение для проверки параметров, переместив его в начало функции. Когда список пуст, мы сразу возвращаемся, потому что обрабатывать нечего.
Как правило, вы можете использовать следующий шаблон для удаления лишнего уровня отступа.
# before
def func():
if guard:
# do something
# do something
# do something
# after
def func():
if not guard:
return
# do something
# do something
# do something
Вложенный код часто возникает во время разработки. В большинстве случаев черновик первого кода не самый лучший с точки зрения удобочитаемости и восприятия. Поэтому, закончив работу с тем или иным фрагментом кода, имеет смысл его доработать.
Предложение Guard – ценный прием рефакторинга, помогающий улучшить код, делая его более плоским и читабельным.
Иногда функция может начинаться с условия, которое проверяет параметры на пригодность. В результате вся функция может оказаться внутри одного условного оператора, что искусственно усложняет ее. Инвертируя условие, мы можем уменьшить дополнительный уровень отступа, сделав код более понятным.
Оригинал