Контекст

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

Понимание списка

Допустим, вы пытаетесь перебрать строку букв и хотите сохранить их в списке, например:

# looping through the alphabet and appending it to a list
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# traditional loop and append
alpha_list = [] # create a list to store
for letter in alphabet: # actual loop
    
    alpha_list.append(letter) # use append
    
print(alpha_list)
# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

Если вы знакомы с Python, то вы, вероятно, знакомы с этим подходом. Мы создали список для хранения нужных нам значений и использовали функцию .append() для добавления каждого элемента или буквы в наш список. В нашем простом примере для этого потребовалось 3 строки кода, но в более сложных программах это может занять несколько строк в зависимости от логики, которую вы хотите реализовать.

Итак, как мы можем сделать это в одной строке? Python делает это так же просто:

# looping through the alphabet and appending it to a list
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# list comprehension
alpha_list = [letter for letter in alphabet]
print(alpha_list)
# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

Вместо того, чтобы объявлять alpha_list как пустую переменную, понимание списка позволяет нам выполнять две операции в одной строке: создание списка и добавление списка. Это уже довольно полезно, но что, если я скажу вам, что мы можем добавить к этому логику?

Скажем, нам нужны только гласные буквы алфавита (A, E, I, O, U). Сначала нам нужно создать массив гласных для сравнения. В традиционном цикле for для этого нам пришлось бы использовать оператор if:

# looping through the alphabet and appending it to a list
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# traditional loop and append
vowel_list = [] # create a list to store
vowels = "AEIOU"
for letter in alphabet: # actual loop
    
    if letter in vowels:
        
        vowel_list.append(letter) # use append
    
print(vowel_list)
# ['A', 'E', 'I', 'O', 'U']

Однако с пониманием списка мы можем реализовать ту же логику в той же строке:

# declare vars
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
vowels = "AEIOU"
# list comprehension with if
vowel_list = [letter for letter in alphabet if letter in vowels]
print(vowel_list)
# ['A', 'E', 'I', 'O', 'U']

Очень круто, правда? А что, если бы нас заботили не буквы, а их положение в алфавите, и мы хотели бы создать список, в котором говорилось бы, является ли положение этой буквы четным или нечетным числом? Здесь мы будем использовать else для достижения этого:

# looping through the alphabet and appending it to a list
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# traditional loop and append
position_list = [] # create a list to store
# actual loop, use enumerate to get index
# append even if index num is even, else odd
# +1 since python index starts at 0
for index, letter in enumerate(alphabet): 
    
    if (index+1) % 2 == 0:
        
        position_list.append("even") 
    
    else:
        position_list.append("odd")
    
print(position_list)
# ['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even']

Опять же, списковые включения сокращают эту программу для достижения тех же результатов с правильным синтаксисом:

# looping through the alphabet and appending it to a list
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# list comprehension with if-else
# use enumerate to get index
position_list = ["even" if (index+1) % 2 == 0 else "odd" for index, letter in enumerate(alphabet)]
print(position_list)
# ['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even']

Важно отметить структуру понимания, как только мы добавим else к нашей логике. Структура может быть примерно разделена между ключевыми словами Python for _ in _ , if и else , причем if и else являются необязательными.

Часть for _ in _ всегда идет первой, за исключением случаев, когда задействована часть else. В этом случае for _ in _ является последней частью нашего понимания. Я указываю на это, потому что это обычно вызывает путаницу у тех, кто плохо знаком с Python.

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

Понимание словаря

Точно так же, как генераторы списков минимизируют код, необходимый для создания списка новых значений, генераторы словарей делают создание пар данных ключ-значение простым и понятным. Продолжая пример позиции в алфавите, давайте создадим словарь позиций индекса алфавита в качестве ключей и букв в качестве значений:

# fill dictionary with for loop
alpha_dict = {}
for index, letter in enumerate(alphabet):
    
    alpha_dict[index+1] = letter
    
print(alpha_dict)
# {1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H', 9: 'I', 10: 'J', 11: 'K', 12: 'L', 13: 'M', 14: 'N', 15: 'O', 16: 'P', 17: 'Q', 18: 'R', 19: 'S', 20: 'T', 21: 'U', 22: 'V', 23: 'W', 24: 'X', 25: 'Y', 26: 'Z'}
# --------------------------VS-----------------------
# fill dictionary with dict comprehension
alpha_dict = {index+1:letter for index, letter in enumerate(alphabet)}
print(alpha_dict)
# {1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H', 9: 'I', 10: 'J', 11: 'K', 12: 'L', 13: 'M', 14: 'N', 15: 'O', 16: 'P', 17: 'Q', 18: 'R', 19: 'S', 20: 'T', 21: 'U', 22: 'V', 23: 'W', 24: 'X', 25: 'Y', 26: 'Z'}

Хотя оба метода подходят, понимание словаря использует способность Python сокращать такие операции и, таким образом, делает код более питоновским. Как и в случае со списками, вы также можете добавить логику с помощью операторов if-else. Обратите внимание, что хотя в приведенном выше примере выполняется итерация по двумитерируемым объектам (алфавитному индексу и алфавиту) для пар ключ:значение с помощью функции enumerate(), вы все равно можете использовать словарное понимание с однойитерацией:

# create dict with base num as key and squared as values
squared_dict = {x:x**2 for x in range(1,11)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}

Установить понимание

Наборы по-своему похожи на списки, но обладают уникальными свойствами, которые их отличают:

  1. Списки допускают дубликаты, а наборы не допускают. Например, каждый элемент множества уникален.
  2. Хотя наборы считаются неупорядоченными, начиная с Python 3.7, наборы упорядочены по времени вставки. Однако, если вы запустите функцию print() для набора, она будет напечатана неупорядоченно. Списки представляют собой упорядоченные последовательности элементов.
  3. Наборы и списки являются изменяемыми. Изменяемость просто означает, что внутреннее состояние структуры данных может быть изменено после объявления. Обратите внимание, что хотя наборы изменяемы, мы не можем изменить элемент в наборе путем индексации или нарезки, как в списках. Мы можем только добавлятьновые элементы в набор, но не изменять их.
  4. Списки могут быть индексированы, а наборы — нет.

Несмотря на эти различия, включение для наборов работает так же, как и для списков:

# set comprehension
alpha_set = {letter for letter in alphabet}
print(alpha_set)
# {'W', 'K', 'D', 'B', 'L', 'T', 'O', 'C', 'M', 'P', 'H', 'I', 'R', 'X', 'V', 'A', 'S', 'Y', 'J', 'F', 'Z', 'G', 'E', 'N', 'U', 'Q'}
# sets remove duplicates
num_list = [1,2,7,4,2,8,3,1,7,9]
num_set = {num for num in num_list}
print(num_set)
# {1, 2, 3, 4, 7, 8, 9}

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

new_list = list({x for x in old_list})

Если вы все еще хотите сохранить список, но удалить дубликаты, используйте приведенное выше.

Как насчет кортежей?

В Python нет такой вещи, как понимание кортежей, поскольку синтаксис для этого используется для выражений генератора:

# generator expression
alpha_gen = (letter for letter in alphabet)
print(alpha_gen)
# <generator object <genexpr> at 0x000002971FA52040>

Это создает объект-генератор, который нам нужно будет прокрутить, чтобы увидеть результаты. Если вы хотите имитировать то, что будет «пониманием кортежа», вы можете сделать что-то вроде этого:

# "tuple comprehension"
alpha_tuple = tuple((letter for letter in alphabet))
print(alpha_tuple)
# ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')

Это использует функцию tuple() в Python для преобразования объекта генератора в кортеж. В отношении кортежей следует отметить, что они неизменяемы, могут быть индексированы и представляют собой упорядоченную последовательность. Используйте их вместо списков, если вы не планируете изменять внутреннее состояние вашей структуры данных.

Заключительные мысли

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

Последний пример, чтобы продемонстрировать это. Давайте посмотрим, сколько времени потребуется для цикла for, map() и понимания списка для выполнения одной и той же операции. Мы можем использовать библиотеку timeit, чтобы проверить, сколько времени это займет:

import timeit
nums = [num for num in range(100)]
# for map test
def square_nums(num):
    
    return num**2
# map test
def square_with_map():
    
    return map(square_nums, nums)
# for loop test
def square_with_loop():
    
    new_nums = []
    
    for num in nums:
        
        new_nums.append(num**2)
        
    return new_nums
# comprehension test
def square_with_comp():
    
    return [num**2 for num in nums]
# tests
print('map result:', timeit.timeit(square_with_map, number=100))
print('loop result:', timeit.timeit(square_with_loop, number=100))
print('comprehension result:', timeit.timeit(square_with_comp, number=100))
# map result: 1.8099992303177714e-05
# loop result: 0.003074599982937798
# comprehension result: 0.002053200005320832

timeit — отличный способ проверить, сколько времени занимает выполнение кода в Python. Результаты в этом фрагменте кода представляют собой среднее время, которое потребовалось для выполнения каждой функции 100 раз. map() заняло меньше всего времени, в то время как циклы заняли больше всего времени, причем значительно (обратите внимание на e-05 для результата map()). Следующий график развивает это дальше и показывает результаты после 10 000 раз (100 выполнений на итерацию из 100 итераций/испытаний):

Как видите, map() сам по себе обеспечивает значительно меньшие накладные расходы по сравнению с циклами for и пониманием. Имеет ли это значение для вашей программы или нет, зависит от того, производительность является ключевым моментом в вашем сценарии. Если нет, то лучшим выбором будет понимание, поскольку вы ничем не жертвуете ради более чистого кода. Однако, если это так, вы можете использовать map().

Хотя понимания являются одними из самых удобных функций Python, для них есть время и место, и со временем и практикой вы научитесь их идентифицировать.

Свяжитесь со мной в LinkedIn или Twitter, чтобы увидеть, что я опубликую дальше!