Сделайте свой Python код читабельнее и производительнее
Python – язык программирования общего назначения, широко используемый в научных вычислениях, искусственном интеллекте, веб-разработке, финансовом моделировании и во многих других областях. Основная причина его популярности заключается в гибкости – есть множество решений для разного рода операций. Однако, в большинстве случаев есть всего лишь одно решение, которое считается предпочтительным среди опытных Python программистов. В этой статье я бы хотел сделать обзор 10 характерных для этого языка практических примеров, которые можно оценить и взять на вооружение для рефакторинга кода на Python.
1. Включение (прим. пер.: абстракции) списков, словарей и множеств
Есть три типичных изменяемых (прим. пер.: мутабельных) контейнерных типа данных в Python: списки, словари и множества. Если у нас есть итерируемый объект, то мы можем воспользоваться циклом for, чтобы перебрать этот объект для создания на его основе нового списка. Однако характерный для Python способ – использовать включение списка, синтаксис которого выглядит следующим образом: [выражение for x in итерируемый_объект if условие]. Следует заметить, что часть с вычисляемым условием необязательна. Давайте посмотрим на следующий код:
>>> # Начнем с создания списка чисел
>>> 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]
>>>
>>> # Характерный для Python способ:
>>> squares1 = [x*x for x in numbers if x%2 == 0]
>>> print(squares1)
[0, 4, 16]
Включение списка
Помимо включения списка мы также можем использовать включения для словарей и множеств, что соответствует терминам «включение словаря» и «включение множества». Для включения словаря используется синтаксис: {выражение_ключа: выражение_значения for элемент in итерируемый_объект}, в то время как синтаксис включения множества – {выражение for элемент in итерируемый_объект}. Данный код демонстрирует их использование:
>>> # Включение словаря
>>> {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}
Включения словаря и множества
2. F-строки
Строки – привычный примитивный тип данных, который мы используем почти во всех проектах. В большинстве случаев для отображения строковых данных нам нужно сделать дополнительный шаг — определенным образом отформатировать их. Можно выполнить форматирование в стиле языка C, что подразумевает использование символа %, либо воспользоваться методом format, которым обладают строки в Python.
Однако в одном из недавних релизов 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
>>>
>>> # Характерный для Python способ:
>>> print(f'1 USD = {usd_to_eur:.2f} EUR')
1 USD = 0.89 EUR
F-строки
Естественно, вышеприведенный код показывает только основы использования f-строк, которые в сущности реализуют почти все стили форматирования, поддерживаемые форматированием в стиле C или методом str.format. Можете прочитать больше о f-строках в официальной документации или в моей недавней статье.
3. Множественное присваивание и распаковка кортежей
При работе с переменными определение одной переменной на одной строке – устоявшаяся практика. Однако при объявлении нескольких переменных мы можем сделать это на одной строке. Во избежание путаницы, пожалуйста, обратите внимание, что таким образом допускается объявлять переменные, которые связаны по смыслу. Если они используются для разных целей, то я бы не рекомендовал делать их множественное присваивание. Давайте посмотрим на рефакторинг:
>>> # Вместо этого:
>>> code = 404
>>> message = "Not Found"
>>>
>>> # Характерный для Python способ:
>>> 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
Распаковка кортежа
4. Распаковка с ловушками
В предыдущем разделе мы рассмотрели, как распаковать кортеж, используя наиболее базовый формат – количество переменных соответствовало количеству элементов кортежа. Однако, когда в кортеже есть несколько элементов, иногда может понадобиться распаковать, используя метод ловушек (прим. пер.: англ. catch-all method – под ловушкой подразумеваю переменную с префиксом-звездочкой, принимающую/захватывающую некоторое количество элементов контейнера при его распаковке, т. е. это не общепринятый термин, т. к. общепринятого не нашел, но это лучшее, что пришло на ум). А именно – все элементы, которые явно не обозначены переменными, будут захвачены переменной с префиксом-звездочкой. Для получения того же результата, нехарактерный для Python подход обычно подразумевает использование срезов, что чревато ошибками, если мы зададим некорректные индексы.
>>> # Определяем кортеж для распаковки
>>> 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]
>>>
>>> # Характерный для Python способ:
>>> first_number1, *middle_numbers1, last_number1 = numbers
>>>
>>> # Проверяем результаты
>>> print(first_number0 == first_number1)
True
>>> print(middle_numbers0 == middle_numbers1)
False
>>> print(last_number0 == last_number1)
True
Распаковка с ловушками
Как вы могли заметить, значения middle_numbers0 и middle_numbers1 не равны. Это потому, что распаковка с ловушками (с использованием звездочки) по умолчанию генерирует объект списка. Таким образом, чтобы у распакованных данных на выходе был тот же тип данных, мы можем использовать конструктор кортежа, как показано ниже:
>>> # Конвертируем распакованный список в кортеж
>>> print(middle_numbers0 == tuple(middle_numbers1))
True
Создание кортежа из переменной-ловушки
5. Выражение присваивания
В выражении присваивания, более известном как «моржовое выражение», используется «оператор-морж» :=, который напоминает моржа с парой глаз и бивнями, не правда ли? Как понятно из его имени, выражение присваивания позволяет присваивать значение переменной и, в то же время, оно может быть использовано в качестве выражения, например таком, как условный оператор. Определение звучит запутанно, но давайте посмотрим на его использование в следующем фрагменте кода:
>>> # Определим некоторые вспомогательные функции
>>> 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()
...
>>> # Характерный для Python способ:
>>> if account_number := get_account("123-45-6789"):
... withdraw_money(account_number)
... else:
... found_no_account()
Выражение присваивания
Как показано выше, когда мы не используем выражение присваивания, нам приходится сначала получить номер счета, а потом использовать его для операции снятия денег (прим. пер.: функция withdraw_money), что создает некоторое дублирование кода. В противоположность этому, мы можем избавиться от одной строки кода с помощью выражения присваивания, которое вызывает функцию и присваивает возвращенное ею значение новой переменной, которая в то же время вычисляется.
Некоторые бы стали утверждать, что экономия одной строки кода не так важна, но это делает более явным наше намерение – переменная account_number актуальна только в области видимости условного оператора. Если у вас есть опыт программирования на Swift, то использование выражения присваивания в условном операторе очень похоже на технику опционального связывания (прим. пер.: или опциональный биндинг), как показано ниже. По сути временная переменная accountNumber, если она содержит значение, используется только в идущей далее области видимости. Таким образом, вам следует познакомиться с выражением присваивания, и через некоторое время вы будете считать такой код более читабельным.
if let accountNumber = getAccount("123-45-6789") {
withdrawMoney(accountNumber)
} else {
foundNoAccount()
}
Опциональное связывание
6. Итерирование с использованием функции enumerate
Почти в каждом проекте нам неминуемо приходится заставлять нашу программу повторять определенные операции для всех элементов в списке, кортеже или каком-либо другом контейнерном типе данных. Добиться выполнения этих повторяющихся операций можно с помощью цикла for. Как правило, мы можем использовать его базовую форму: for элемент in итерируемый_объект. Однако, при таком итерировании, если нам нужен подсчет текущей итерации цикла, лучше воспользоваться функцией 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
>>>
>>> # Характерный для Python способ:
>>> for i, student in enumerate(students, 1):
... print(f"# {i}: {student}")
...
# 1: John
# 2: Jack
# 3: Jennifer
# 4: June
Итерирование с помощью enumerate
7. Связывание итерируемых объектов с помощью zip/zip_longest
Предположим, что мы у нас есть два итерируемых объекта, и мы хотим объединить эти два объекта для каждой соответствующей пары элементов. Обычно, можно воспользоваться методом обращения по индексу, извлекая элементы из каждого итерируемого объекта таким образом, чтобы иметь возможность объединять их для формирования словаря. Однако, в 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, каждый элемент которого представляет собой кортеж, состоящий из элементов итерируемых объектов, переданных в функцию. Еще стоит отметить, что по умолчанию функция zip остановится, когда будет в каком-либо итерируемом объекте закончатся элементы. В противоположность этому, функция zip_longest будет ориентироваться на наиболее длинный итерируемый объект.
Приведенный выше характерный для Python подход использует преимущество конструктора dict, которое заключается в возможности использовать итерируемый объект для создания объекта класса dict. Помимо представленного выше варианта использования, объект класса zip может быть использован напрямую, как это показано ниже.
>>> for student, grade in zip(students, grades):
... print(f"{student}: {grade}")
...
John: 95
Mike: 90
Sam: 98
David: 97
Итерирование с использованием функции zip
8. Конкатенация (прим. пер.: сцепление) итерируемых объектов
В примере выше мы использовали функцию 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)
...
>>> # Характерный для Python способ:
>>> for student in chain(group0, group1):
... present_project(student)
Сцепление итерируемых объектов
Как показано выше, нехарактерный для Python подход подразумевает необходимость создания дополнительных списков — не самый эффективный с точки зрения памяти путь. В отличие от этого, функция chain создает итерируемый объект из тех итерируемых, которые были предварительно определены. Более того, функция chain гибкая и может принимать итерируемые объекты любого типа: словари, множества, списки, объекты классов zip и map (посредством использования функции map), а также многие другие типы итерируемых объектов Python.
9. Тернарное выражение
Что если нам нужно присвоить что-либо переменной таким образом, чтобы на основе условия были присвоены разные значения? В этом случае мы можем использовать условный оператор, чтобы вычислить условие и определить, какое значение должно быть использовано для присваивания. Обычно это подразумевает несколько строк кода. Однако, чтобы справиться с этой работой, мы можем использовать тернарное выражение, которое занимает всего лишь одну строку кода и обладает следующим основным синтаксисом: var = значение_в_случае_true if условие else значение_в_случае_false. Давайте рассмотрим соответствующие примеры.
>>> # Определяем вспомогательную функцию
>>> 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"
...
>>> # Характерный для Python способ:
>>> state = "Even" if got_even_number() else "Odd"
Тернарное выражение
10. Использование генераторов
Концепция генераторов может быть не знакома новичкам в 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)
...
>>> # Характерный для Python способ:
>>> with open('large_file') as file:
... for row_data in file:
... process_data(row_data)
Генераторы
Заключение
Есть много еще более предпочтительных, естественных для Python способов осуществления разного рода вещей в Python, и темы, затронутые в этой статье – лишь неполный список приемов, которые я счел полезными в своих повседневных проектах. Надеюсь, они помогут писать код на Python и вам. Заключительный совет – вы должны целенаправленно делать рефакторинг своего кода, используя эти и другие характерные для Python приемы (прим. пер.: идиомы), которые обычно еще и более эффективные с точки зрения производительности. С помощью единообразного рефакторинга своего кода вы будете постепенно улучшать свои навыки программирования на Python.
Спасибо за чтение этого материала, и удачного программирования.