Python — это язык программирования общего назначения, который широко используется в таких областях, как научные вычисления, искусственный интеллект, веб-разработки, финансовое моделирование и многих других. Одна из главных причин популярности этого языка заключается в его гибкости, предполагающей возможность множественных решений для различных операций.
Однако в большинстве случаев из всего доступного разнообразия опытные программисты Python предпочитают использовать только одно возможное и зачастую нерациональное решение. Вот поэтому в данной статье мне бы хотелось рассмотреть 10 оптимальных решений, которые вы можете использовать для рефакторинга кода на Python.
В Python существуют 3 общеизвестных изменяемых контейнера: списки, словари и множества. Если мы начинаем с итерируемого объекта, то возможен вариант с применением цикла for для его перебора и создания на его основе нового списка. Однако в этом случае оптимальное использовать представление списков со следующим синтаксисом: [expression for x in iterable if any_condition]
. Обратите внимание, что определение условия является необязательной частью. Рассмотрим представление списков.
>>> # Сначала создаем список чисел
>>> numbers = list(range(5))
>>> print(numbers)
[0, 1, 2, 3, 4]
>>>
>>> # Вместо следующего способа:
>>> squares0 = []
>>> for number in numbers:
... if number%2 == 0:
... squares0.append(number*number)
...
>>> print(squares0)
[0, 4, 16]
>>>
>>> # Используем оптимальный вариант:
>>> squares1 = [x*x for x in numbers if x%2 == 0]
>>> print(squares1)
[0, 4, 16]
Помимо списков можно также использовать представление словарей и множеств, синтаксис которых выглядит следующим образом: для словарей — {key_expr: val_expr for item in iterable}
, для множеств — {expr for item in iterable}
. В следующем коде представлены примеры их применения.
>>> # Представление словаря
>>> {x: x*x for x in range(5)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
>>>
>>> # Представление множества
>>> {x*x for x in range(5)}
{0, 1, 4, 9, 16}
Строки являются стандартным базисным типом данных, который используется практически во всех наших проектах. В большинстве случаев для отображения данных строки требуется форматировать их особым образом. Это можно сделать при помощи метода в стиле C, подразумевающего использование символа % или метода format.
Однако в последних релизах Python был представлен новый компактный и хорошо читаемый метод форматирования строк. Он известен как f-строки, что значит форматированные строковые литералы. Проведем сравнение подходов.
>>> # Определим коэффициент конверсии
>>> usd_to_eur = 0.89
>>>
>>> # Вместо следующего способа:
>>> print('1 USD = {rate:.2f} EUR'.format(rate=usd_to_eur))
1 USD = 0.89 EUR
>>> print('1 USD = %.2f EUR' % usd_to_eur)
1 USD = 0.89 EUR
>>>
>>> # Используем оптимальный вариант:
>>> print(f'1 USD = {usd_to_eur:.2f} EUR')
1 USD = 0.89 EUR
Конечно, данный пример только отображает самый основной случай использования f-строк, которые в действительности реализуют почти все способы форматирования, поддерживаемые методами в стиле C или format. Вы можете продолжить знакомство с ними, обратившись к официальной документации или к моей статье на данную тему.
Работая с переменными, мы обычно определяем по одной из них в каждой строке. Однако при объявлении множественных переменных можно сделать это и в одной. Во избежании недопонимания следует отметить, что данный способ касается объявления семантически однородных переменных. Если же они служат разным целям, то я бы не советовал его использовать. Рассмотрим следующий рефакторинг:
>>> # Вместо следующего способа:
>>> code = 404
>>> message = "Not Found"
>>>
>>> # Используем оптимальный вариант:
>>> code, message = 404, "Not Found"
По своей внутренней логике множественные присваивания включают создание кортежа на правой стороне и его распаковку на левой. Указанный ниже код демонстрирует процесс распаковки кортежа. Как видно из примера, он выглядит как множественное присваивание, так как в его основе лежит тот же самый механизм.
>>> # Определяем кортеж и распаковываем его
>>> http_response = (404, "Not Found")
>>> code, message = http_response
>>> print(f'code: {code}; message: {message}')
code: 404; message: Not Found
В предыдущем разделе мы рассмотрели процесс распаковки кортежа самым основным способом, в котором число переменных соответствует числу определяемых элементов в объекте. Но в случае с кортежем, содержащим множественные элементы, иногда может потребоваться распаковка при помощи метода catch-all. Точнее говоря, все элементы, напрямую не обозначенные переменными, будут подхвачены переменной с символом * в качестве префикса. Есть и другой способ достичь того же результата, но он уже не такой оптимальный. Речь идет о методе среза (slice) для представления некоторой части последовательности, но он сопряжен с ошибками в случае пропуска правильных индексов.
Распаковка методом catch-all:
>>> # Определяем кортеж для распаковки
>>> numbers = tuple(range(8))
>>> numbers
(0, 1, 2, 3, 4, 5, 6, 7)
>>>
>>> # Вместо следующего способа:
>>> first_number0 = numbers[0]
>>> middle_numbers0 = numbers[1:-1]
>>> last_number0 = numbers[-1]
>>>
>>> # Используем нерациональный вариант:
>>> first_number1, *middle_numbers1, last_number1 = numbers
>>>
>>> # Проверяем результаты
>>> print(first_number0 == first_number1)
True
>>> print(middle_numbers0 == middle_numbers1)
False
>>> print(last_number0 == last_number0)
True
Как вы могли заметить, значения middle_numbers0
и middle_numbers1
не равноценны. Причина этого состоит в том, что распаковка методом catch-all (с использованием *
) генерирует объект списка по умолчанию. Следовательно, для приведения итоговых распакованных данных к одному типу можно использовать конструктор tuple()
, как показано в примере:
>>> # Конвертируем распакованный список в кортеж
>>> print(middle_numbers0 == tuple(middle_numbers1))
True
Выражение присваивания больше известно как “моржовое” выражение с использованием “моржового” оператора (walrus operator) :=
, вид которого напоминает одноименное животное с парой глаз и бивнями. Как следует из названия, данное выражение позволяет присваивать значение переменной, и в то же время может быть использовано в качестве выражения, к примеру в инструкции условия if
. Поскольку определение звучит несколько запутанно, обратимся к примеру использования в следующем фрагменте кода:
>>> # Определяем вспомогательные функции
>>> def get_account(social_security_number):
... pass
...
>>> def withdraw_money(account_number):
... pass
...
>>> def found_no_account():
... pass
...
>>> # Вместо следующего способа:
>>> account_number = get_account("123-45-6789")
>>> if account_number:
... withdraw_money(account_number)
... else:
... found_no_account()
...
>>> # Используем оптимальный вариант:
>>> if account_number := get_account("123-45-6789"):
... withdraw_money(account_number)
... else:
... found_no_account()
...
Как показано выше, когда мы не используем выражение присваивания, нам приходится сначала получить номер счета и применить его к операции по снятию денег, что сопровождается созданием дублированного кода. В отличие от этого, мы можем исключить одну строку кода с помощью выражения присваивания, которое вызывает функцию и присваивает возвращаемое значение новой переменной, которая вычисляется параллельно.
Кто-то может сказать, что сохранение одной строки кода не играет большой роли, но мы тем самым проясняем свое намерение относительно переменной account_number
, которая действует только в области оператора if
. Если у вас есть опыт программирования на Swift, то использование выражения присваивания в операторе if
во многом идентично методу необязательной привязки, представленному ниже. По сути, временная переменная accountNumber
используется в последующей области видимости, только когда она действительна. Таким образом, с выражением присваивания стоит познакомиться, и со временем вы обнаружите, что ваш код стал более читаемым.
if let accountNumber = getAccount("123-45-6789") {
withdrawMoney(accountNumber)
} else {
foundNoAccount()
}
Практически в каждом проекте мы неизбежно вынуждаем нашу программу повторять отдельные операции со всеми элементами списка, кортежа или других контейнеров. Эти повторяющиеся операции можно выполнить с помощью цикла for. Обычно используется основная форма: for item in iterable
. Однако для итерации, если требуется вести счет циклов, лучше использовать функцию enumerate
, которая создаст счетчик. Более того, у нас есть возможность установить число, с которого он будет начинать отсчет.
Итерация с enumerate
:
>>> # Создаем список студентов для итерации
>>> students = ['John', 'Jack', 'Jennifer', 'June']
>>>
>>> # Вместо следующего способа:
>>> for i in range(len(students)):
... student = students[i]
... print(f"# {i+1}: {student}")
...
# 1: John
# 2: Jack
# 3: Jennifer
# 4: June
>>>
>>> # Используем оптимальный вариант:
>>> for i, student in enumerate(students, 1):
... print(f"# {i}: {student}")
...
# 1: John
# 2: Jack
# 3: Jennifer
# 4: June
Предположим, что мы начинаем с двух итерируемых объектов, и нам нужно объединить их c каждой соответствующей парой элементов. По обыкновению, мы можем использовать метод индексации, извлекая элементы из каждого итерируемого объекта для их объединения с целью создания словаря. Однако в Python есть встроенная функция zip, выполняющая как раз то, что нам нужно. По сути, она принимает несколько итерируемых объектов и создает из них один, длина которого соответствует самому короткому из числа переданных. Рассмотрим следующий пример использования функции zip с итерируемыми объектами:
>>> # Создаем два списка объектов для функции zip
>>> students = ['John', 'Mike', 'Sam', 'David', 'Danny']
>>> grades = [95, 90, 98, 97]
>>>
>>> # Вместо следующего способа:
>>> grade_tracking0 = {}
>>> for i in range(min(len(students), len(grades))):
... grade_tracking0[students[i]] = grades[i]
...
>>> print(grade_tracking0)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97}
>>>
>>> # Используем оптимальный вариант:
>>> grade_tracking1 = dict(zip(students, grades))
>>> print(grade_tracking1)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97}
>>>
>>> from itertools import zip_longest
>>> grade_tracking2 = dict(zip_longest(students, grades))
>>> print(grade_tracking2)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97, 'Danny': None}
Функция zip создает zip
— объект-итератор, чьи элементы являются кортежами, содержащими элементы из полученных итерируемых объектов. Следует отметить, что по умолчанию эта функция останавливается, достигнув конца любой последовательности. А вот функция zip_longest
, наоборот, будет использовать самый длинный итерируемый объект.
Вышеописанный оптимальный способ использует преимущество конструктора dict
, способного применить итерируемый объект для создания объекта dict
. Помимо уже рассмотренного примера, zip-объект может быть напрямую использован в итерации:
>>> for student, grade in zip(students, grades):
... print(f"{student}: {grade}")
...
John: 95
Mike: 90
Sam: 98
David: 97
В приведенном выше примере мы использовали функцию zip для объединения итерируемых объектов по элементам. А что же делать, если согласно конкретной бизнес-задаче мы должны их конкатенировать (объединить в цепочку)? Допустим, требуется перебрать два итерируемых объекта с элементами одной категории для одной и той же операции. Для этой цели можно использовать функцию chain
. Обратимся к следующему примеру сцепления итерируемых объектов:
>>> # Определяем данные и вспомогательную функцию
>>> from itertools import chain
>>> group0 = ['Jack', 'John', 'David']
>>> group1 = ['Ann', 'Bill', 'Cathy']
>>>
>>> def present_project(student):
... pass
...
>>> # Вместо следующего способа:
>>> for group in [group0, group1]:
... for student in group:
... present_project(student)
...
>>> for student in group0 + group1:
... present_project(student)
...
>>> # Используем оптимальный вариант:
>>> for student in chain(group0, group1):
... present_project(student)
...
Как мы видим, отличный от оптимального способ требует создания дополнительных списков, что чревато излишними затратами памяти. В то же время функция chain
создает итерируемый объект из уже имеющихся последовательностей. Кроме того, ей свойственна гибкость, поэтому она может принимать любые виды итерируемых объектов в Python, включая словари, множества, списки, zip-объекты, map-объекты (при помощи функции map
) и многие другие.
Мы можем присвоить переменной разные значения с учетом заданных условий. Для этой цели существует оператор if
, устанавливающий условие и определяющий значение, которое необходимо присвоить. Обычно данная операция занимает несколько строк кода. Однако мы можем воспользоваться тернарным выражением для решения нашей задачи, используя одну строку со следующим основным синтаксисом: var = true_value if condition else false_value
. Рассмотрим соответствующий пример использования:
>>> # Определяем вспомогательную функцию
>>> from random import randint
>>> def got_even_number():
... return randint(1, 10) % 2 == 0
...
>>> # Вместо следующего способа:
>>> if got_even_number():
... state = "Even"
... else:
... state = "Odd"
...
>>> state = "Odd"
>>> if got_even_number():
... state = "Even"
...
>>> # Используем оптимальный вариант:
>>> state = "Even" if got_even_number() else "Odd"
Идея генераторов может быть незнакома новичкам в Python, поскольку во многих других языках программирования данный метод не столь широко распространен. Этот хитрый прием позволяет вам работать с потоком данныхбез необходимости его установки в начальную точку. Вместо этого генератор выдает следующее значение по запросу, что способствует эффективному использованию памяти.
Рассмотрим следующий простой пример. Предположим, перед нами стоит задача обработать большие объемы данных в файле. Теоретически мы можем считать весь файл в список и обработать в нем каждую строку данных. Однако если файл окажется очень большим, то велика вероятность, что ваш компьютер быстро исчерпает свой ресурс памяти. Поэтому следует воспользоваться более оптимальным и лучшим решением, сделав из файла генератор, возвращающий каждый раз только одну строку кода по требованию.
>>> # Определяем вспомогательную функцию
>>> def process_data(row_data):
... pass
...
>>> # Вместо следующего способа:
>>> with open('large_file') as file:
... read_data = file.read().split("\n")
... for row_data in read_data:
... process_data(row_data)
...
>>> # Используем оптимальный вариант:
>>> with open('large_file') as file:
... for row_data in file:
... process_data(row_data)
...
Заключение
В Python существует гораздо больше оптимальных вариантов выполнения различных операций, и в данной статье отражены только некоторые из них — в основным это те эффективные приемы, которые я использую в своих повседневных проектах. Надеюсь, они вам также помогут в процессе написания кода на Python. Рекомендую вам проводить рефакторинг, намеренно используя характерные для данного языка методы, которые обычно отличаются большей производительностью. Систематически осуществляя переработку кода, вы постепенно улучшите свои навыки программирования.
Благодарю за внимание! Да будет программирование в радость!
Перевод статьи Yong Cui, Ph.D.: 10 Idiomatic Ways to Refactor Your Python Code
Комментарии