Списки в Python

Оглавление

Введение

Списки легко распознать в Python. Всякий раз, когда мы видим скобки '[]', мы знаем, что речь идет о списках. Объявлять списки в Python очень просто.

Вот прохождение.

Первый шаг:

my_list = []

Затем, если мы хотим добавить что-либо в список, мы просто вызываем:

my_list.append() # one element

или

my_list.extend() # several elements

Ничего не может быть проще для чтения, и если ваши списки не сломаны, зачем их исправлять? На самом деле есть две основные причины, и даже одна бонусная:

Двумя основными причинами являются:

  1. Вы сможете быстрее создавать рабочий код.
  2. Код со списковыми включениями даже легче читать (с небольшой практикой).

Причина бонуса:

  1. Списочные компы имеют умеренное преимущество в производительности.

( Примечание: Вы можете встретить 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 по структурам данных, чтобы изучить различные возможности пониманий, а затем поэкспериментируйте с этими идеями в своих собственных проектах.

Вернуться на верх