Источник: Nuances of Programming
При создании любого проекта, независимо от его размера, важно обращать внимание на его обслуживаемость. База кода всегда должна быть удобной в этом отношении, чтобы в долгосрочной перспективе избежать временных затрат на стадии поддержания проекта. Вот почему данный аспект разработки никогда не следует упускать из виду.
Один из основных способов сделать проект более легким в обслуживании — писать краткий код. Во-первых, потому что такой код не создает проблем при чтении и понимании для коллег по команде. Во-вторых — чем лаконичнее код, тем его меньше, а значит, он менее подвержен ошибкам.
В данной статье мы рассмотрим ряд функциональностей 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 , поэтому нет необходимости сравнивать их с другим значением.
- Именованные кортежи — это легкий в реализации и гибкий контейнер данных, предназначенный только для их чтения.
- Частичные функции устраняют необходимость повторять общие параметры внутри конкретной области видимости.
Читайте также:
Перевод статьи Yong Cui : Apply These 4 Techniques To Write Concise Python Code