Структуры данных

Списки/массивы

Список работает как набор «безымянных переменных», называемых его элементами. Элементы нумеруются последовательными натуральными числами (начиная с нуля).

Элемент списка можно получить при помощи формулы список[номер], где список — формула, значением которой является интересующий нас список, а номер — формула, значением которой является интересующий нас номер элемента.

Узнать количество элементов списка можно функцией len.

Изменить элемент списка можно командой изменения. Например:

foo[3] = 5  ## привязать третий элемент к пятёрке

Очень важный момент: каждый списковый литерал создаёт новый список, а перепривязка переменной — нет.

foo = [1,2,3,4,5]
bar = foo         # bar привязана к тому же списку, что и foo
bar[0] = 5        # теперь и foo[0] == 5
baz = [1,2,3,4,5] # а это -- уже совсем другой список
bar[1] = baz[1]   # первый элемент списка bar теперь привязан к 
                  # значению формулы baz[1], то есть числу 2 
                  # (но не к первому элементу списка baz!)

Чтобы скопировать часть списка (то есть создать новый список с теми же привязками), можно воспользоваться срезом:

foo = [1, [2, 3], 4]
bar = foo[1:3] # копируем элементы списка с 1-го (включая) по 3-й (исключая)
bar[1] = 5     # foo[2] по-прежнему 4
bar[0][0] = 5  # теперь и foo[1][0] == 5: срез делает "неглубокую" копию

В срезе можно опустить один или оба индекса. Тогда они считаются равными нулю и количеству элементов списка соответственно. В частности, foo[:] делает копию всего списка foo.

Также допускаются срезы с тремя индексами (подробности — в документации).

Добавить новые элементы к списку можно при помощи метода (атрибута-подпрограммы) append: foo.append(123) добавляет в конец списка foo элемент, привязанный к числу 123.

Убрать элемент с конца списка можно методом pop вот так: foo.pop(). Хотя pop можно применять и к конкретному номеру, удаление элементов из списка занимает время, пропорциональное разности длины списка и номера удаляемого элемента.

List comprehesion

Также часто можно встретить т.н. «теоретико-множественную» запись:

[x ** 2 for xs in ys for x in xs if x < 3]

В общем случае такая запись выглядит как расположенная внутри квадратных скобок формула, за которой следует цепочка for ... in ... или if ....

Вычисление такой формулы происходит ровно так же, как и вычисление переменной result в следующем коде:

result = []

for xs in ys:
    for x in xs:
        if x < 3:
            result.append(x ** 2)

То есть соответствующие циклы и ветвления распологаются в той же последовательности друг внутри друга, а формула становится второй подформулой оператора вызова функции (первой подформулой которого становится result.append).

Естественно, никакой переменной result при вычислении «теоретико-множественных записей» не происходит.

Кортежи

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

foo = 1, 2, 3
bar = (1, 2, 3)

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

Несмотря на неизменяемость кортежей, никто не мешает на основе список и кортежей делать, например, «списки постоянной длины»:

foo = ([1], [2], [3])
foo[1][0] = 5  # теперь foo == ([1], [5], [3])

# можно даже спрятать соответствующие формулы с [0] за функциями
def get_elem(a, i): return a[i][0]
def set_elem(a, i, v): a[i][0] = v
# теперь, не зная о внутренней структуре "списков постоянной длины", их
# "элементы" невозможно (зло)намеренно удлинить

Тексты

Ведут себя как кортежи одноэлементых текстов (как бы странно подобная рекуррентная семантика ни выглядела). Например, "f", "foo"[0], "foo"[0][0], "foo"[0][0][0] и так далее — одно и то же.

В отличие от кортежей, операция конкатенации весьма эффективна. То есть ожидаемой квадратичной зависимости времени работы от значения переменной N следующий код не проявляет:

text = ""
for x in range(N):
    text += str(x)

Отметим также многострочные литералы, оформляющиеся тройными кавычками. Эти литералы зачастую используются вместо многострочных комментариев (которых в языке нет).

Ещё стоят упоминания появившиеся сравнительно недавно (на момент 2022 года) в языке интерполирующие литералы, оформляющиеся буквой f перед кавычками. Внутри таких литералов можно заключать в фигурные скобки произволные формулы, значения которых имеют текстовое представление.

То есть вместо str(3) + " + " + str(5) + " = " + str(3+5) рекомендуется писать f"{3} + {5} = {3+5}".

Байтовые кортежи

Функция bytes позволяет преобразовать последовательность чисел, находящихся в пределах от 0 до 255, в байтовый кортеж. Байтовые кортежи требуются некоторыми функциями ввода-вывода.

Отметим, что в Python встроен кодировщик/декодировщик текстовой кодировки UTF-8:

  • bytes(текст, "utf-8") — байты кодировки текста
  • str(байты, "utf-8") — текст, закодированный байтами

Словари

Позволяют произвольный неизменяемый (числа, тексты, кортежи) ключ привязать к произвольному значению.

{}  ## пустой словарь
{ "foo": 3, 3: "bar", 4: None }  ## ключ "foo" привязан к 3
                                 ## ключ 3 привязан к "bar"
                                 ## ключ 4 привязан к None

foo = {}
foo["foo"] = 3
foo[3] = "bar"
foo[4] = None  ## теперь переменная foo привязана к словарю, 
               ## эквивалентному таковому из второй строчки

del foo[3]  ## привязки можно удалять; это -- быстрая операция
3 in foo    ## наличие привязки можно проверить; это -- быстрая операция
len(foo)    ## количество привязок можно посчитать; тоже быстрая операция

Следует быть очень аккуратным при использовании цикла for: если for с массивом проходится по значениям (то есть по привязкам чисел-ключей), то for со словарём проходится по самим ключам. Поэтому, к сожалению, зачастую нельзя подменять массивы словарями без существенной переработки кода, использовавшего эти массивы.

Также нужно учитывать, что for проходится по словарю в произвольном порядке (конкретный произвол которого в районе версии языка 3.6 сильно менялся).

Множества

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

set()          ## пустое множество
{1, 2, "foo"}  ## трёхэлементное множество

foo = set()
foo.add(1)
foo.add(2)
foo.add("foo")  ## теперь переменная foo привязана к множеству, 
                ## эквивалентному таковому из второй строчки