Код
#База знаний

Как улучшить код на Python: приёмы рефакторинга

Чтобы код оставался понятным, в нём регулярно надо убираться. Рассказываем, что и где прибирать.

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

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

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

Если вы только-только столкнулись с рефакторингом — сперва прочтите эту статью.

Убираем мусор в коде

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

Закомментированный код

Удаляйте закомментированные куски кода. Они сбивают с толку разработчиков, которые работают с исходниками после вас.

Например, вы попробовали вариант решения задачи, но потом решили сделать всё по-другому. Промежуточные куски кода вы закомментировали — а вдруг пригодятся. Не стоит так делать. Если понадобится, лучше посмотреть предыдущую версию кода в системе контроля версий.

Отладочные команды print()

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

Ненужные команды импорта

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

Не забудьте удалить из кода соответствующие команды импорта — обычно редакторы их подсвечивают. Так, в IDE PyCharm текст команды становится бледным и при компиляции выдаётся предупреждение «Unused import statement…».

Неиспользуемые переменные

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

Найти эти переменные легко — редакторы их подсвечивают, а при компиляции выдаётся предупреждение: «Variable is not used».

Например:

Здесь PyCharm выделяет серым и ненужный модуль os, и неиспользуемые переменные unused1 и unused2.

Улучшаем читаемость кода

Проводя рефакторинг, важно оформить код так, чтобы его было удобно читать.

Создатель языка Python Гвидо ван Россум сказал: «Код читают гораздо чаще, чем пишут».

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

Гвидо ван Россум считал удобочитаемость одним из важнейших принципов языка Python. Поэтому вместе с соратниками разработал свод рекомендаций по оформлению кода — PEP 8. Рассмотрим некоторые из них подробнее.

Имена переменных

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

Например, имя переменной в выражении sc += 1 нам мало о чём говорит.

А если изменить его на score, то станет ясно, что речь идёт о счёте.

Есть два важнейших правила именования сущностей в Python — их нарушение вызывает ошибку:

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

PEP 8 предписывает задавать имена определённым образом:

  • Имена функций и модулей пишутся строчными буквами. Они могут состоять из одного или нескольких слов, которые разделяются подчёркиванием (например: function, my_function).
  • Имена переменных тоже задаются в нижнем регистре. Они могут состоять из одной буквы, слова или нескольких слов. Слова в составе имени тоже разделяются подчёркиванием: x, variable, my_var.
  • Название константы задаётся символом, словом или несколькими словами в верхнем регистре. Слова в составе имени разделяются знаком подчёркивания.

    Например: C, CONST, MY_CONST.
  • Имена классов состоят из одного (Class) или нескольких слов (MyClass) без разделителей.

    Во втором случае, как вы заметили, каждое слово начинается с прописной буквы. Такой стиль называется CamelCase («верблюжий стиль»).
  • Имена модулей задаются в нижнем регистре. Они не должны быть длинными. Знак подчёркивания уместен, если с ним имя модуля читабельнее, а его назначение понятнее.

    Например: pyclbr, py_compile.

    Форматирование кода

    PEP 8 описывает и принципы оформления кода:

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

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

    Здесь линтер flake8 в PyCharm выдаёт предупреждения о лишних пробелах внутри скобок и перед ними.

    Для разных языков программирования есть свои линтеры. Например, для HTML — Beautify, для JavaScript — JSLint и так далее.

    Популярные линтеры для Python:

    Все они легко интегрируются в среды разработки. А самым умным считается pylint. Он проверяет:

    • все ли подключённые модули используются в программе;
    • есть ли лишние переменные;
    • не превышает ли длина строк 79 символов;
    • правильно ли заданы имена;
    • верно ли используются аргументы;
    • есть ли синтаксические ошибки;
    • как используются типы данных
    • и многое другое.

    Обнаруженные ошибки можно исправить вручную или с помощью утилит для форматирования кода — форматтеров, которые совместимы с IDE PyCharm или Visual Studio.

    Популярные форматтеры:

    Рефакторинг для сокращения кода

    Избавляемся от дублирования

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

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

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

    • Возникают куски одинакового текста, различающиеся лишь некоторыми строками или символами — и не сразу понятно какими. Это затрудняет понимание программы.
    • Текст программы раздувается, в нём труднее разобраться.
    • Когда нужно менять функциональность, за которую отвечает дублирующийся код, изменения приходится вносить сразу во много мест программы. Это чревато ошибками.

    Чтобы избавиться от дублирования, применяют приём рефакторинга «извлечение метода»: дублирующийся участок выносят в отдельный метод или функцию и везде заменяют вызовом этого метода/функции.

    Рассмотрим на примере:

    while True :
     a1 = int(input( "Введите цену товара: "))
     if a1 < 0 :
       print ("Значение не должно быть отрицательным!")
     else :
       break
    
    while True :
     a2 = int(input("Введите количество товара: "))
     if a2 < 0 :
       print ("Значение не должно быть отрицательным!")
     else :
       break
    
    a3 = a1 * a2
    print("Стоимость товара: ", a3)

    Анализ кода

    Этот код явно нуждается в рефакторинге. И вот почему:

    • два блока выполняют практически одинаковые действия;
    • названия переменных a1, a2, a3 не говорят об их предназначении.

    Мы видим в коде два блока, которые отличаются только сообщениями пользователю: «Введите цену товара: » и «Введите количество товара: ».

    Такое дублирование — это плохо:

    • Например, поменялись бизнес-правила: нас больше не интересуют товары с нулевой стоимостью. И нужно поставить условие: количество и цена должны быть больше нуля. Нам придётся вносить изменения в оба блока кода.
    • Или мы захотим, скажем, добавить в расчёт стоимости скидку/наценку. Тогда нам придётся создать ещё один похожий блок с новым сообщением: «Введите процент скидки (или наценки) на товар: ». А потом ещё один и ещё. Код быстро разрастётся.

    Извлечение метода

    Чтобы упростить код, преобразуем цикл while в функцию, а сообщения для пользователя будем передавать в неё как аргументы. Назовём функцию input_value() («ввод значения»), добавим в неё команду return a1 и уберём break.

    Вот что у нас выйдет:

    def input_value() :
      while True :
         a1 = int (input("Введите цену товара: "))
         if a1 < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return a1

    Чтобы сообщать пользователю, что вводить (цену, количество товара и так далее), вместо приглашения «Введите цену товара: » добавим параметр функции и назовём его prompt («подсказка»).

    Вот что получится:

    def input_value(prompt):
      while True:
         a1 = int(input(prompt))
         if a1 < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return a1

    Теперь заменим одинаковые блоки кода вызовами функции с соответствующими значениями параметра. Сразу же дадим переменным новые, осмысленные имена: value, price, quantity, cost.

    Исправленный код будет выглядеть так:

    def input_value(prompt):
      while True:
         value = int( input( prompt ) )
         if value < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return value
    
    price = input_value("Введите цену товара: ")
    quantity = input_value("Введите количество товара: ")
    cost = price * quantity
    print("Стоимость товара: ", cost)

    Встраивание переменной

    Переменная cost не используется в дальнейших вычислениях: она всего лишь хранит результат подсчёта стоимости товара для передачи его в качестве аргумента функции print().

    Поэтому переменную cost можно смело убирать, а в print () передавать выражение price * quantity. Это сократит код, улучшит читабельность и уменьшит используемые ресурсы:

    def input_value(prompt):
      while True:
         value = int( input( prompt ) )
         if value < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return value
    
    price = input_value ("Введите цену товара: ")
    quantity = input_value ("Введите количество товара: ")
    print ("Стоимость товара: ", price * quantity)

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

    Проверьте, что переменная, которую вы собираетесь удалить, не из таких.

    Что получили

    В результате рефакторинга:

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

    Программа стала короче, нагляднее и вдобавок универсальнее. Например, теперь легко добавить скидку в расчёт стоимости товара:

    def input_value(prompt):
      while True:
         value = int( input( prompt ) )
         if value < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return value
    
    price = input_value ("Введите цену товара: ")
    quantity = input_value ("Введите количество товара: ")
    discount = input_value ("Введите процент скидки: ")
    print ("Стоимость товара: ", price * quantity * (1 - discount/100))

    Используем эквивалентные замены

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

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

    Рассмотрим несколько простых приёмов рефакторинга, которые позволяют уменьшить объём кода.

    Генераторы списков вместо цикла for

    Для создания списков, словарей и множеств в языке Python часто применяют генераторы. Один из практикуемых приёмов рефакторинга — заменить таким генератором цикл for. Код сокращается, становится нагляднее, к тому же работает быстрее.

    Пример первый

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

    Реализация с использованием цикла:

    spisok = [1, 25, 44, 213, 14, 27, 56, 8]
    square_spisok = []
    for item in spisok:
       square_spisok.append(item**2)
    print(square_spisok)
    
    --OUTPUT>
    [1, 625, 1936, 45369, 196, 729, 3136, 64]
    

    После рефакторинга с переходом к генератору:

    spisok = [1, 25, 44, 213, 14, 27, 56, 8]
    square_spisok = [item**2 for item in spisok]
    print(square_spisok)
    
    --OUTPUT>
    [1, 625, 1936, 45369, 196, 729, 3136, 64]
    

    Пример второй

    Вычисление суммы двух матриц. С циклами реализация выглядит так:

    a = [[1, 2, 3], [11,12,13], [21,22,23], [31,32,33]]
    b = [[41, 42, 43], [51,52,53], [61,62,63], [71,72,73]]
    summa_a_b = [[0,0,0], [0,0,0], [0,0,0], [0,0,0]]
    for i in range(len(a)):
       for j in range(len(a[0])):
           summa_a_b[i][j] = a[i][j] + b[i][j]
    
    print(summa_a_b)
    
    --OUTPUT>
    [[42, 44, 46], [62, 64, 66], [82, 84, 86], [102, 104, 106]]
    

    Заменим цикл генератором списка:

    a = [[1, 2, 3], [11,12,13], [21,22,23], [31,32,33]]
    b = [[41, 42, 43], [51,52,53], [61,62,63], [71,72,73]]
    summa_a_b = [[a[i][j] + b[i][j] for j in range(len(a[0]))]
                for i in range(len(a))]
    
    print(summa_a_b)
    
    --OUTPUT>
    [[42, 44, 46], [62, 64, 66], [82, 84, 86], [102, 104, 106]]
    

    После рефакторинга код стал короче.

    Функция enumerate() вместо range()

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

    Иногда для этого создают переменную-счётчик и с её помощью перебирают элементы в цикле for, например:

    spisok = [154,127,98,89,77,55,32,14]
    for i in range(len(spisok)):
       print(i + 1, spisok[i])
    
    --OUTPUT>
    1 154
    2 127
    3 98
    4 89
    5 77
    6 55
    7 32
    8 14
    

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

    Можно улучшить код — использовать функцию enumerate(), которая перебирает элементы контейнера без промежуточной переменной. У этой функции два аргумента: переменная контейнерного типа (spisok) и начальное значение счёта (переменная i). Зададим второму аргументу значение 1, чтобы счёт начинался с единицы.

    После рефакторинга код будет выглядеть так:

    spisok = [154,127,98,89,77]
    for i, spisok in enumerate(spisok, 1):
       print(i, spisok)
    
    --OUTPUT>
    1 154
    2 127
    3 98
    4 89
    5 77
    

    Тернарное выражение вместо if…else

    Если переменная принимает разные значения в зависимости от того, выполняется или нет какое-то условие, то условный оператор if…else можно заменить тернарным выражением.

    Рассмотрим пример:

    def even_odd(number):
       if number % 2 == 0:
           state = "Even"
       else:
           state = "Odd"
       return state
    print(even_odd(128))

    Заменим условие тернарным выражением:

    def even_odd(number):
       return "Even" if number % 2 == 0 else "Odd"

    Код стал короче и проще. Но у этого приёма есть ограничения — его стоит применять только к простым условным выражениям. В результате рефакторинга не должны появляться вложенные тернарные операторы, потому что их очень сложно читать.

    Например:

    def f_if_else(x,y):
       if (x==1):
           if(y==2):
               return 1
           else:
               return 2
       else:
           if(y==3):
                return 3
           else:
                return 4

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

    def f_nested_if_else(x,y):
       return ((1) if (y == 2) else (2)) if (x == 1) else ((3) if (y == 3) else (4))

    Код стал короче, но разобраться в нём теперь намного сложнее.

    Множественное присваивание

    Присвоить значение сразу нескольким переменным в Python можно одной строкой.

    Пользоваться этой возможностью надо аккуратно — не усложнять понимание программы. Чтобы не запутаться, в одну строку объявляют переменные, которые связаны по смыслу.

    Пример первый

    name1 = 'Аня'
    name2 = 'Катя'
    name3 = 'Антон'
    name4 = 'Света'

    Эти переменные можно объявить в одной строке. И код станет короче:

    name1,name2,name3,name4 = 'Аня', 'Катя', 'Антон', 'Света'
    
    

    Пример второй

    Иногда переменным нужно присвоить диапазон значений:

    one = 1
    two = 2
    three = 3
    four = 4
    print(' one is',one,'\n', 'two is', two, '\n', 'three is', three, '\n', 'four is', four)
    
    --OUTPUT>
    one is 1 
    two is 2 
    three is 3 
    four is 4
    

    В этом случае Python позволяет использовать множественное присваивание совместно с функцией range():

    one, two, three, four = range(1,5)
    print(' one is',one,'\n', 'two is', two, '\n', 'three is', three, '\n', 'four is', four)

    Функция zip для связывания объектов

    Предположим, у нас есть два списка: первый — с именами друзей, а второй — это города, где они живут. Нужно объединить элементы списков в словарь вида «имя: город».

    Это можно сделать с помощью цикла for:

    friends = ["Катя", "Лена", "Даша", "Костя", "Дима"]
    cities = ["Москва", "Пенза", "Екатеринбург", "Минск"]
    friends_cities = {}
    for i in range(min(len(friends), len(cities))):
       friends_cities[friends[i]] = cities[i]
    print(friends_cities)
    
    --OUTPUT>
    {'Катя': 'Москва', 'Лена': 'Пенза', 'Даша': 'Екатеринбург', 'Костя': 'Минск'}
    

    Однако в Python встроена функция zip, которая делает то же самое — короче и проще.

    Её аргументами могут быть списки, кортежи, множества и словари. Функция zip возвращает итератор, объединяющий элементы из объектов-источников.

    Например, передадим в zip аргументами строку, список и множество:

    arg1 = 'абвгдеж'
    arg2 = ['1','2','3','4','5']
    arg3 = {'!','@','#','$','%','^'}
    
    zip_obj = zip(arg1, arg2, arg3)
    print(zip_obj)
    
    for i in zip_obj:
       print(i)
    
    --OUTPUT>
    <zip object at 0x000001D228F06BC0>
    ('а', '1', '!')
    ('б', '2', '@')
    ('в', '3', '#')
    ('г', '4', '$')
    ('д', '5', '%')
    

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

    Теперь создадим словарь «имя: город» с использованием zip. Код будет выглядеть так:

    friends = ["Катя", "Лена", "Даша", "Костя", "Дима"]
    cities = ["Москва", "Пенза", "Екатеринбург", "Минск"]
    friends_cities = dict(zip(friends, cities))
    print(friends_cities)
    
    --OUTPUT>
    {'Катя': 'Москва', 'Лена': 'Пенза', 'Даша': 'Екатеринбург', 'Костя': 'Минск'}
    

    Функция zip работает до тех пор, пока не закончатся элементы в самом коротком из итерируемых объектов-аргументов.

    Если нужно ориентироваться на самый длинный список, то используют функцию zip_longest из модуля itertools:

    from itertools import zip_longest
    
    friends = ["Катя", "Лена", "Даша", "Костя", "Дима"]
    cities = ["Москва", "Пенза", "Екатеринбург", "Минск"]
    friends_cities = dict(zip_longest(friends, cities))
    print(friends_cities)
    
    --OUTPUT>
    {'Катя': 'Москва', 'Лена': 'Пенза', 'Даша': 'Екатеринбург', 'Костя': 'Минск', 'Дима': None}
    

    Инструменты рефакторинга в PyCharm

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

    Подготовка

    Открываем IDE и устанавливаем параметры рефакторинга в настройках редактора (File | Settings | Editor | Code Editing) в разделе Refactorings.

    Если мы выберем режим In modal dialogs (во всплывающих окнах), то при использовании инструментов Extract/Introduce редактирование имён параметров, констант, полей и переменных будет происходить в открывающихся модальных окнах:

    Если же мы предпочтём режим In the editor (в редакторе), то имена параметров, констант, полей и переменных будем редактировать прямо в редакторе:

    Для примера установим In modal dialogs.

    Теперь создадим в окне редактора новый проект (File | New Project) с именем refactorProject. Добавим в него новый файл (File | New | Python File), а назовём его, скажем, refactor.py.

    Разберём извлечение метода в PyCharm на одном из наших примеров:

    while True :
     a1 = int(input( "Введите цену товара: "))
     if a1 < 0 :
       print ("Значение не должно быть отрицательным!")
     else :
       break
    
    while True :
     a2 = int(input("Введите количество товара: "))
     if a2 < 0 :
       print ("Значение не должно быть отрицательным!")
     else :
       break
    
    a3 = a1 * a2
    print("Стоимость товара: ", a3)

    Копируем этот код в окно редактора:

    Извлечение метода в PyCharm

    Выделим первый цикл while и используем инструмент Refactor | Extract/Introduce | Method из главного или контекстного меню (горячие клавиши Ctrl + Alt + M).

    В нужное поле открывшегося окна вводим название функции — input_value ():

    Чтобы добавить параметр функции, выделим подсказку «Введите стоимость товара: » и выберем в главном/контекстном меню инструмент Refactor | Introduce Parameter (Ctrl + Alt + P):

    В появившемся модальном окне введём название параметра — prompt (в переводе с английского — «подсказка»):

    У нас получилось:

    def input_value(prompt):
      while True:
         a1 = int(input(prompt))
         if a1 < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return a1

    Переименование в PyCharm

    Заменим название переменной a1 на value с помощью инструмента рефакторинга «Переименование». Выделим a1 и выберем Refactor | Rename (Shift + F6):

    Дадим новым переменным осмысленные имена: price, quantity, cost.

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

    def input_value(prompt):
      while True:
         value = int( input( prompt ) )
         if value < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return value
    
    price = input_value("Введите цену товара: ")
    quantity = input_value("Введите количество товара: ")
    cost = price * quantity
    print("Стоимость товара: ", cost)

    Встраивание переменной

    Как мы помним, переменной cost присваивается результат простого выражения (price * quantity) и больше с ней ничего не делается.

    Поэтому нам можно от неё избавиться — заменить обращения к этой переменной выражением price * quantity. Сделаем это с помощью инструментов рефакторинга.

    Выделим переменную cost и выберем в меню Refactor инструмент Inline («Встраивание») или вызовем его горячими клавишами Ctrl + Alt + N.

    PyCharm определит число вхождений и попросит подтвердить действие:

    Нажимаем OK. Программа убирает выражение, в котором создаётся переменная cost, и везде подставляет вместо неё price * quantity:

    def input_value(prompt):
      while True:
         value = int( input( prompt ) )
         if value < 0 :
             print ("Значение не должно быть отрицательным!")
         else :
             return value
    
    price = input_value ("Введите цену товара: ")
    quantity = input_value ("Введите количество товара: ")
    print ("Стоимость товара: ", price * quantity)

    Чем объёмнее код, тем выгоднее применять инструменты рефакторинга. С ними уборка в коде ускоряется и ошибки сводятся к минимуму.

    Подытожим

    Мы рассмотрели лишь несколько приёмов рефакторинга — в Python их намного больше. Будучи быстрыми и несложными, вместе они здорово повышают качество проекта. Главное — проводить рефакторинг регулярно и не забывать затем тестировать приложение.


    Проверьте свой английский. Бесплатно ➞
    Нескучные задания: small talk, поиск выдуманных слов — и не только. Подробный фидбэк от преподавателя + персональный план по повышению уровня.
    Пройти тест
    Понравилась статья?
    Да

    Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

    Ссылка скопирована