Списки в Python
Оглавление
- Введение
- Когда использовать ListComps
- Получение практики
- Продвинутое использование
- Преимущества производительности
- Куда двигаться дальше
Введение
Списки легко распознать в Python. Всякий раз, когда мы видим скобки '[]', мы знаем, что речь идет о списках. Объявлять списки в Python очень просто.
Вот прохождение.
Первый шаг:
my_list = []
Затем, если мы хотим добавить что-либо в список, мы просто вызываем:
my_list.append() # one element
или
my_list.extend() # several elements
Ничего не может быть проще для чтения, и если ваши списки не сломаны, зачем их исправлять? На самом деле есть две основные причины, и даже одна бонусная:
Двумя основными причинами являются:
- Вы сможете быстрее создавать рабочий код.
- Код со списковыми включениями даже легче читать (с небольшой практикой).
Причина бонуса:
- Списочные компы имеют умеренное преимущество в производительности.
( Примечание: Вы можете встретить List Comprehensions, называемые по-разному: comprehensions, List Comprehensions или ListComps.)
Причина, по которой я считаю, что преимущество в производительности не является основным фактором, заключается в том, что если производительность является основной проблемой, вам, возможно, стоит обратить внимание на другой тип данных - например, Dictionary.
Для начинающих пользователей List Comprehensions или Dictionary Comprehensions может быть трудно понять, как они могут быть проще, чем старое доброе объявление списка и манипулирование им с помощью явных методов. Ответ - практика и место для ошибок. В ListComp гораздо меньше места для ошибок, чем во вложенном цикле for. Поскольку понимание обычно (но не обязательно) происходит в одной строке, мозг может переварить больше смысла за один раз.
С практикой вы будете предпочитать читать код, написанный с использованием понятий, потому что:
- Вы автоматически знаете предполагаемый выход.
- Вы можете видеть входные данные и то, как они изменяются.
- Подобно лямбда-функциям, вы можете легко передавать ListComps динамически.
Когда использовать ListComps
Что именно такое List Comprehensions, и зачем они нам нужны, если списки настолько гибкие из коробки?
Списковые включения используются, когда список должен быть заполнен последовательно в соответствии с известным шаблоном, часто для извлечения данных из существующей коллекции.
Например, допустим, у вас есть некоторые JSON данные из API, которые при разборе библиотекой запросов оказываются списком из тысяч телефонных номеров, каждый из которых хранится как словарь с несколькими полями:
phones = [
{
'number': '111-111-1111',
'label': 'phone',
'extension': '1234',
},
{
'number': '222-222-2222',
'label': 'mobile',
'extension': None,
}
]
Что, если бы нашим заданием было просто вывести список чисел?
Конечно, мы можем итерировать список традиционно:
my_phone_list = []
for phone in phones:
my_phone_list.append(phone['number'])
>>>my_phone_list
['111-111-1111', '222-222-2222']
Понятие списка, которое достигает того же результата, занимает всего одну строку:
>>>[phone['number'] for phone in phones]
['111-111-1111', '222-222-2222']
Списковое включение следует основной схеме...
[ <do something to item> for <item> in <list>]
... или если вы хотите сохранить результат:
output_list = [ <manipulate item> for <item> in <input list> ]
Помните, что если список кажется запутанным, как это часто бывает поначалу, не стесняйтесь разбить его на цикл for:
<output list>
For <item> in <input list>:
output_list.append(< manipulate item>)
Получение практики
Поскольку List Comprehensions приводит к такому же выводу, как и цикл for, лучше всего думать о том, как бы вы переписали цикл for каждый раз, когда используете его. Просто помните, что всякий раз, когда вы видите for
, вы должны спросить: "А как бы это выглядело, если бы это был List Comprehension?"
Без написания кода, вы знаете, что он был бы короче, во-первых!
Далее, просто подумайте, куда бы вы поместили это выражение for
:
[ … <for item in list>]
^ Start with brackets, and put your for expression at the end.
Наконец, решите, какие элементы должны быть в вашем списке вывода, и поместите их в самое начало:
[ <output items> for … in …. ]
^ Right at the beginning.
Наконец, посмотрите, не выдает ли ваша IDE или интерпретатор ошибок, и проверьте синтаксис.
Поздравляем! Вы только что попрактиковались в понимании списков. Теперь повторите, и вы быстро научитесь думать на языке понимания.
Расширенное использование
Вложенные списки-комплекты
Списки в Python часто бывают вложенными, поэтому, конечно, мы захотим иметь возможность создавать вложенные списки с помощью ListComps.
И знаете что? Они по-прежнему умещаются на одной строке.
Используем произвольный пример для формы, 3 строки x,y,z.
fields = ['x', 'y', 'z']
rows = [1, 2, 3]
table = []
for r in rows:
row = []
for f in fields:
row.append(f)
table.append(row)
>>>table
[['x', 'y', 'z'], ['x', 'y', 'z'], ['x', 'y', 'z']]
Теперь посмотрим, будет ли это легче для глаз:
table = [[f for f in fields] for row in rows]
Все еще в замешательстве? Вложенные списки могут быть сложными поначалу, но просто подумайте о ваших входных и выходных списках и о том, где они находятся в синтаксисе.
Здесь мы хотим получить список списков - список строк. Итак, мы знаем, что наш вывод должен быть строкой, которая представляет собой список значений.
Поскольку наш вывод - это список, то это то, что идет первым!
[< give me a list > for row in rows]
^^ The output is a list
Мы можем посмотреть на наш цикл for выше, чтобы понять это, или просто подумать о самом простом понимании в нашей голове:
[f for f in fields] # you don't *have* to do anything to f
Теперь, поскольку мы просто хотим сделать это для каждого элемента в рядах (по сути, диапазон), мы просто скажем, что!
[[f for f in fields] for row in rows]
Или еще проще...
[fields for row in rows]
Первая версия более полезна, если вам нужно каким-то образом манипулировать f
. Попробуйте выполнить функции внутри ListComp:
>>> [[print(f) for f in fields] for row in rows]
x
y
z
x
y
z
x
y
z
[[None, None, None], [None, None, None], [None, None, None]]
Сначала для элемента выполняется функция print()
, затем возвращаемое значение передается в список. Это простой способ выполнить работу над элементом, а затем проверить, успешно ли функция выполнилась для каждого из них.
Обратите внимание, что возвращаемый список - это не тот список, который вам нужен, а состоит из результатов оценки функции.
Предположим вместо этого, что поля были целыми числами, которые нужно было преобразовать в строки, в этом случае вы могли бы сделать что-то вроде:
>>>fields = [123,456,789]
>>>[[str(f) for f in fields] for row in rows]
[['123', '456', '789'], ['123', '456', '789'], ['123', '456', '789']]
Предположим, что все строки имеют разные значения, списковые понимания предлагают очень лаконичный и читабельный способ применения этой функции ко всем значениям.
В реальном мире это простой способ заполнить таблицу для отправки в API, который требует многомерных массивов (подсказка: это отличный способ массового обновления Google Sheet!). Синтаксически гораздо проще передать ListComp в пост-запрос, чем каждый раз писать цикл for перед запросом
Понимание словаря
Мы говорили об использовании ListComps для передачи форматированной информации в словари, но разве не было бы здорово, если бы вы могли создавать словари так же, как мы создавали списки?
Хорошая новость: вы можете: они называются Dictionary Comprehensions.
Здесь есть два разных случая использования, которые мы должны различать. Построение списка словарей все еще технически является ListComp, поскольку наш выход - это список словарей, но это быстрый способ отобразить значения в список словарей:
>>> [{str(item):item} for item in [1,2,3,]]
[{'1': 1}, {'2': 2}, {'3': 3}]
Словарь Comprehension принимает любой входной сигнал и выдает словарь, если вы назначите ключ и значение в области "сделать что-то" в начале.
{v:k for (k, v) in my_dict.items()}
^^ Associate key and value here.
В отличие от приведенного выше списка словарей, мы хотим получить на выходе словарь. Итак, давайте начнем со словаря, который служит той же цели, что и карта преобразования строк в целые числа:
dict_map = {'apple' : 1,
'cherry': 2,
'earwax': 3,}
Возможно, нам нужно поменять значения местами, чтобы наша карта работала с какой-нибудь функцией. Мы можем написать цикл for и просмотреть словарь, меняя местами ключи и значения. Или мы можем использовать Dictionary Comprehension, чтобы сделать то же самое в одной строке. Скобки дают нам знать, что мы хотим получить на выходе словарь:
>>>{v:k for (k, v) in dict_map.items()}
{1: 'apple', 2: 'cherry', 3: 'earwax'}
Все, что мы сделали, это изменили порядок на противоположный для каждого кортежа, возвращаемого .items()
. Если вы практикуетесь в чтении и написании понятий, этот вариант в одну строку гораздо более читабелен и, следовательно, питоничен, чем цикл for.
Логика и сравнения в списковых осмыслениях
Одной из самых мощных возможностей списковых пониманий является возможность условно передавать значения в список с помощью логических операторов. Хотя нам нужно помнить, что нужно "сделать что-то" заранее, в начале понимания, мы также можем "фильтровать" ввод внутри понимания в той же строке.
Начнем с самого простого примера. У нас есть список [1,2,3]
. Но нам нужны только значения меньше 3. Чтобы отфильтровать ненужные нам значения, используем comprehension:
>>>values = [1,2,3]
>>>[i for i in values if i < 3]
[1, 2]
Давайте посмотрим на наш список словарей, составленный ранее:
dict_map = {
'apple' : 1,
'cherry': 2,
'earwax': 3,
}
Возможно, целочисленные значения представляют собой цены за кусок пирога, а у нас есть только два дублона. Мы можем сформировать список с помощью понимания, а затем условно отсеять значения, которые нам не нужны, или использовать логику внутри спискового включения:
>>>[k for k, v in dict_map.items() if v < 3]
['apple', 'cherry']
Возможностей для этого паттерна больше, чем мы можем рассмотреть здесь, но с помощью вложенных, отфильтрованных пониманий вы можете вывести практически любой список, который только можно себе представить. Чтобы попрактиковаться, начните с обычной итерации, а затем подберите строки вашего цикла к элементам понимания.
Преимущества производительности
Списковое включение не делает то же самое, что и цикл for, который добавляет список. Результаты одинаковы, но под капотом они работают немного по-разному. Чтобы понять это, мы можем посмотреть на байткод Python, создаваемый как циклом for, так и пониманием.
Рассмотрим следующий пример, используя строку в качестве входного списка:
original_string = 'hello world'
spongecase_letters = []
for index, letter in enumerate(original_string):
if index % 2 == 1:
spongecase_letters.append(letter.upper())
else:
spongecase_letters.append(letter)
spongecase_string = ''.join(spongecase_letters)
Сначала определим функцию, чтобы было легче читать:
def spongecase(index, letter):
if index % 2 == 1:
return letter.upper()
else:
return letter
original_string = “hello world”
spongecase_letters = []
for index, letter in enumerate(original_string):
transformed_letter = spongecase(index, letter)
spongecase_letters.append(transformed_letter)
spongecase_string = ‘’.join(spongecase_letters)
# hElLo wOrLd
Синтаксис спискового включения может быть представлен как:
[transformed_item for item in original_list]
или
[item_you_want for item_you_have in original_list]
Итак, давайте попробуем:
[spongecase(index, letter) for index, letter in enumerate(original_string)]
Та-да! У вас остался список того, что вы хотите (буквы, выделенные губкой), учитывая то, что у вас есть (индекс и буква) из исходной строки.
Хотя на практике мы можем рассматривать этот синтаксис как краткий цикл for, это не совсем так.
Байткод - это инструкции, передаваемые интерпретатору, которые определяют, какие команды языка Си нужно выполнить для выполнения кода Python. Существуют байткоды для хранения констант, например, когда мы сохраняем "hello world" в переменную original_string
. Существуют байткоды для загрузки функций, например, когда мы загружаем spongecase
для того, чтобы вызвать ее. Если бы реализация цикла for и понимание списка выполняли совершенно одно и то же, они должны были бы производить одинаковые инструкции байткода для интерпретатора.
Вы можете просмотреть байткод функции с помощью dis, который является частью стандартной библиотеки, поэтому если мы упакуем наши реализации внутри функций, мы сможем сравнить байткод, созданный двумя методами. Фактическое построение списка в двух реализациях отличается: Для цикла for соответствующая секция находится здесь, где мы загружаем функцию spongecase, вызываем ее для преобразования буквы, загружаем метод append и затем вызываем его для добавления преобразованной буквы в список
11 46 LOAD_FAST 0 (spongecase) # loads spongecase function
49 LOAD_FAST 3 (index)
52 LOAD_FAST 4 (letter)
55 CALL_FUNCTION 2 # calls spongecase on index and letter
58 STORE_FAST 5 (transformed_letter)
12 61 LOAD_FAST 2 (spongecase_letters) # loads the spongecase_letters list
64 LOAD_ATTR 1 (append) # loads the append method
67 LOAD_FAST 5 (transformed_letter)
70 CALL_FUNCTION 1 # calls the append method to append transformed_letter to spongecase_letters
Для спискового включения соответствующий раздел выглядит иначе; существует специальный байткод LIST_APPEND
, который выполняет ту же операцию, что и метод append, но имеет свой собственный байткод:
40 LOAD_FAST 0 (spongecase) # loads the spongecase function
43 LOAD_FAST 2 (index)
46 LOAD_FAST 3 (letter)
49 CALL_FUNCTION 2 # calls the spongecase function on index and letter
52 LIST_APPEND 2 # appends the result of the spongecase call to an unnamed list
55 JUMP_ABSOLUTE 28
58 STORE_FAST 4 (spongecase_letters) # stores the resulting list as spongecase_letters
Важным моментом здесь является то, что синтаксис спискового включения на самом деле производит для интерпретатора другие инструкции, чем для цикла for. Эти инструкции оптимизированы для построения списков - например, загрузка метода append занимает ненулевое время, поэтому замена загрузки и вызова функции append на инструкцию, предназначенную специально для добавления в списки, экономит интерпретатору один шаг на каждое добавление или один шаг на каждый элемент исходного списка. Или, говоря иначе, понимания списков - это не просто синтаксический сахар, они действительно более эффективны при построении списков, чем циклы for, потому что они компилируются в несколько иной паттерн байткода, оптимизированный специально для построения списков.
Куда двигаться дальше
Теперь, когда мы рассмотрели элементы и синтаксис List Comprehensions, пришло время немного попрактиковаться. Помните, что элементы должны быть прямыми: "Сделать что-то" впереди, элемент в середине, предоставляемый списком в конце. По желанию фильтруйте входной список с помощью логических операторов. Поскольку понимания читабельны и лаконичны, их можно смело считать питоническими, даже если поначалу они являются загадкой.
Вы никогда не сможете быть "слишком хороши" в List Comprehensions. Часто невероятно сложные итерационные циклы можно заменить одним или двумя ListComps. Это особенно актуально при написании обратных вызовов для веб-фреймворка, такого как Flask, или при работе с API, который возвращает глубоко вложенный JSON. Вам может понадобиться создать простой список или словарь из настоящего леса разветвленных ответов, и List Comprehensions - это то, что вам нужно.
Эта техника особенно полезна при динамической генерации списков на изменяющихся данных, когда логика должна быть отделена от ввода. С другой стороны, если вы можете легко понимать и читать понимания, попробуйте расширить границы с помощью многострочных пониманий, которые условно производят сложный вывод из сложного ввода. Просмотрите раздел документации Python по структурам данных, чтобы изучить различные возможности пониманий, а затем поэкспериментируйте с этими идеями в своих собственных проектах.
Вернуться на верх