При создании любого проекта, независимо от его размера, важно обращать внимание на его обслуживаемость. База кода всегда должна быть удобной в этом отношении, чтобы в долгосрочной перспективе избежать временных затрат на стадии поддержания проекта. Вот почему данный аспект разработки никогда не следует упускать из виду.
Один из основных способов сделать проект более легким в обслуживании — писать краткий код. Во-первых, потому что такой код не создает проблем при чтении и понимании для коллег по команде. Во-вторых — чем лаконичнее код, тем его меньше, а значит, он менее подвержен ошибкам.
В данной статье мы рассмотрим ряд функциональностей Python, позволяющих писать более краткий код.
1. Enumerate в цикле for
Применение циклов for
избавляет от написания повторяющегося кода для одной и той же работы. Во многих случаях требуется регистрация положения элемента в итерируемом объекте. Рассмотрим 2 возможные реализации многословной версии без enumerate
:
# Список гостей
arrived_guests = ["John", "Ashley", "Danny", "Bobby"]
for guest in arrived_guests:
arrived_order = arrived_guests.index(guest) + 1
print(f"# {arrived_order}: {guest}")
for guest_i in range(len(arrived_guests)):
guest = arrived_guests[guest_i]
print(f"# {guest_i + 1}: {guest}")
- Первый цикл
for
содержит методindex()
для определения положения элемента, который извлекается напрямую путем доступа к списку. - Второй цикл
for
включает функциюrange()
для создания итерируемого объекта, производящего индекс, по которому извлекается элемент.
В этих версиях элемент и индекс получаются по отдельности. Однако есть способ сгенерировать обе единицы информации сразу. Следующий код иллюстрирует более краткую реализацию с enumerate
:
for guest_i, guest in enumerate(arrived_guests, 1):
print(f"# {guest_i}: {guest}")
enumerate()
получает в качестве первого параметра список, который производит итератор, содержащий каждый элемент в виде объекта кортежа.- Объект кортежа состоит из 2 компонентов: счетчика (или “индекса”) и элемента. В этом примере для получения к ним прямого доступа мы используем распаковку.
- Второй параметр функции
enumerate()
определяет число, с которого запускается счетчик. В примере установлено значение1
, указывающее на то, что отсчет начинается с1
.
2. Проверка контейнера на пустоту
Как правило, кортежи, списки, словари и множества в Pyhton относятся к контейнерам, поскольку все эти типы данных содержат другие объекты в качестве элементов. Примечательно, что они могут быть пустыми. В связи с этим при работе с такими контейнерами данных зачастую необходимо проверять наличие в них элементов, перед тем как переходить к выполнению других операций.
В качестве примера рассмотрим список, но тот же принцип проверки распространяется и на другие типы данных.
Далее следуют 2 возможные реализации многословной версии:
# Список, полученный от сервера
fetched_data = []
if len(fetched_data) > 0:
print("We fetched some data.")
else:
print("We didn't fetch any data.")
if fetched_data != []:
print("We fetched some data")
else:
print("We didn't fetch any data")
- В первом примере присутствует функция
len()
, проверяющая число элементов в списке. Если его длина превышает0
, значит, он не пустой. - Во втором примере сравниваются значения полученного и пустого списков. Если они не совпадают, то полученный список не пустой.
Следующий код демонстрирует более краткую версию:
if fetched_data:
print("We fetched some data")
else:
print("We didn't fetch any data")
Данный код использует то обстоятельство, что Python оценивает пустой список как False
, а не пустой — как True
. Такого принципа проверки также придерживаются и в отношении других контейнеров: кортежей, словарей и множеств. Кстати говоря, он же подходит и для строк, которые являются True
при условии, что они не пустые.
3. Именованные кортежи в качестве контейнеров данных
Если проект предполагает чтение данных, обладающих одинаковой структурой, то можно прибегнуть к контейнерам, которые позволяют получать доступ к отдельным элементам данных. Допустим, один блок данных содержит 3 единицы информации о клиенте: имя, возраст и пол. Рассмотрим реализации многословной версии:
# Словари
client0 = {"name": "John", "age": 37, "gender": "M"}
client1 = {"name": "Danny", "age": 41, "gender": "M"}
client2 = {"name": "Jennifer", "age": 34, "gender": "F"}
# Пользовательский класс
class Client:
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
client0 = Client("John", 37, "M")
client1 = Client("Danny", 41, "M")
client2 = Client("Jennifer", 34, "F")
- Для представления каждого клиента возможен вариант с использованием словарей. Однако нельзя исключать вероятность ошибок в написании ключей, что приведет к исключениям
KeyError
. - Мы также можем создать пользовательский класс для управления информацией клиента. Но этот способ сопровождается потреблением памяти отдельными объектами и дополнительными затратами ресурсов на надлежащее обслуживание объекта класса.
Если же просто требуется легкий контейнер для хранения данных, и большая часть операций состоит в их чтении, то для этих целей вполне подойдет именованный кортеж. Далее следует соответствующая реализация:
from collections import namedtuple
Client = namedtuple("Client", "name age gender")
client0 = Client("John", 37, "M")
client1 = Client("Danny", 41, "M")
client2 = Client("Jennifer", 34, "F")
namedtuple
— это фабричная функция, доступная в модулеcollections
. Такое название обусловлено тем, что она создает новый тип данных, являющихся подтипом кортежей, как показано ниже:
>>> type(Client)
<class 'type'>
>>> issubclass(Client, tuple)
True
- В функции
namedtuple
мы передаем имя класса в качестве первого параметра, а атрибуты (строку, разделенную пробелами, или список строк) — в качестве второго. - При создании экземпляров класса
Client
можно задействовать тот же самый метод инстанцирования, что и для обычного пользовательского класса. - Более того, есть возможность воспользоваться той же точечной нотацией для обращения к “атрибутам” объекта кортежа аналогично объектам пользовательского класса:
>>> client0 = Client("John", 37, "M")
>>> client0.name
'John'
>>> client0.age
37
>>> client0.gender
'M'
4. Частичные функции
Во избежание повторения кода мы проводим рефакторинг функций. Рассматривая этот процесс в более крупной области видимости, мы получаем следующую вспомогательную функцию и ее применение:
# Общая вспомогательная функция
def save_image_to_directory(image_data, file_name, desired_directory):
print(f"{image_data}, {file_name}, {desired_directory}")
# Событие 0
save_image_to_directory("image_data0_101", "event0_101.png", "folder_for_event0")
save_image_to_directory("image_data0_102", "event0_102.png", "folder_for_event0")
# Много других вызовов
# Событие 1
save_image_to_directory("image_data1_101", "event1_101.png", "folder_for_event1")
save_image_to_directory("image_data1_102", "event1_102.png", "folder_for_event1")
# Много других вызовов
- Вспомогательная функция
save_image_to_directory
используется в различных модулях. - При работе с
Event 0
мы передаем 3 параметра функции. Примечательно, что третий параметр всегда один и тот же в области видимости модуля. - Что касается другого события, то здесь выполняется тот же сценарий с повторением третьего параметра для каждого из вызовов.
Для этого случая больше подходит частичная функция. В особенности если задействуется конкретная функция с одинаковыми параметрами, применяемыми в каждом ее вызове внутри приемлемой области видимости (например, модуле). По сути, частичные функции создаются через использование части параметров к уже существующим функциям. Обратимся к примеру:
from functools import partial
# Событие 0
save_image_for_event0 = partial(save_image_to_directory, desired_directory='folder_for_event0')
save_image_for_event0("image_data0_101", "event0_101.png")
save_image_for_event0("image_data0_102", "event0_102.png")
# Событие 1
save_image_for_event1 = partial(save_image_to_directory, desired_directory='folder_for_event1')
save_image_for_event1("image_data1_101", "event1_101.png")
save_image_for_event1("image_data1_102", "event1_102.png")
- Функция
partial
доступна в модулеfunctools
. Она берет существующую функцию и применяет общий параметр для каждого модуля. В данном случае таким параметром являетсяdesired_directory
. - Функция
partial
создает другую функцию. Ее вызов устраняет необходимость передавать общий параметр. Как видите, с этого момента нужно просто установить 2 параметра частичной функции.
Создать такую функцию также можно, воспользовавшись лямбда-функцией, как показано ниже. Однако это не столь явный, как очевидный прием с частичной функцией.
save_image_for_event2 = lambda x, y: save_image_to_directory(x, y, desired_directory='folder_for_event2')
save_image_for_event2("image_data2_101", "event2_101.png")
Проверка лямбда-функции также проблематична, поскольку она не предоставляет никакой полезной информации в отличие от частичной функции, созданной с помощью partial
. Можете сравнить оба варианта:
>>> save_image_for_event1
functools.partial(<function save_image_to_directory at 0x111bf68b0>, desired_directory='folder_for_event1')
>>> save_image_for_event2
<function <lambda> at 0x111bf6940>
Заключение
В данной статье были рассмотрены 4 функциональности, способствующие написанию более краткого кода Pyhton. С помощью этих техник и многих других подходов вам вполне по силам улучшить общую обслуживаемость проектов.
Подведем краткие итоги:
- Функция
enumerate()
применяется с целью создания счетчиков для элементов итерируемых объектов в циклахfor
. - Python оценивает пустые контейнеры как
False
, поэтому нет необходимости сравнивать их с другим значением. - Именованные кортежи — это легкий в реализации и гибкий контейнер данных, предназначенный только для их чтения.
- Частичные функции устраняют необходимость повторять общие параметры внутри конкретной области видимости.
Читайте также:
- 8 ключевых команд для управления средами Conda
- Классы данных в Python и их ключевые особенности
- Анализ социальных сетей: от теории графов до приложений на Python
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Yong Cui: Apply These 4 Techniques To Write Concise Python Code