Распутываем сильно вложенный код Python

Распутываем сильно вложенный код 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 – ценный прием рефакторинга, помогающий улучшить код, делая его более плоским и читабельным.

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE