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

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

В данной статье мы рассмотрим ряд функциональностей 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, поэтому нет необходимости сравнивать их с другим значением. 
  • Именованные кортежи  —  это легкий в реализации и гибкий контейнер данных, предназначенный только для их чтения.  
  • Частичные функции устраняют необходимость повторять общие параметры внутри конкретной области видимости. 

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yong Cui: Apply These 4 Techniques To Write Concise Python Code

Предыдущая статья15 расширений VSCode, необходимых программистам в 2021 году
Следующая статьяAPI, WebSocket или WebHook: что выбрать?