Контекст
Несмотря на присущую 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}
Установить понимание
Наборы по-своему похожи на списки, но обладают уникальными свойствами, которые их отличают:
- Списки допускают дубликаты, а наборы не допускают. Например, каждый элемент множества уникален.
- Хотя наборы считаются неупорядоченными, начиная с Python 3.7, наборы упорядочены по времени вставки. Однако, если вы запустите функцию
print()
для набора, она будет напечатана неупорядоченно. Списки представляют собой упорядоченные последовательности элементов. - Наборы и списки являются изменяемыми. Изменяемость просто означает, что внутреннее состояние структуры данных может быть изменено после объявления. Обратите внимание, что хотя наборы изменяемы, мы не можем изменить элемент в наборе путем индексации или нарезки, как в списках. Мы можем только добавлятьновые элементы в набор, но не изменять их.
- Списки могут быть индексированы, а наборы — нет.
Несмотря на эти различия, включение для наборов работает так же, как и для списков:
# 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, чтобы увидеть, что я опубликую дальше!