Списки: list, методы, slice, sort vs sorted

list - самая используемая коллекция в Python. Это упорядоченная, изменяемая последовательность элементов любого типа. Под капотом - динамический массив указателей. В этом уроке - операции, срезы (slice), сортировка и типичные идиомы.

Создание списка

# Литерал
nums = [1, 2, 3]

# Конструктор list()
empty = list()              # []
chars = list("hello")       # ['h', 'e', 'l', 'l', 'o']
nums = list(range(5))       # [0, 1, 2, 3, 4]

# Из comprehension
squares = [x ** 2 for x in range(5)]   # [0, 1, 4, 9, 16]

# Повторение
zeros = [0] * 10           # десять нулей
`[0] * 10` для `int` (immutable) работает корректно. Но для [mutable объектов](./09-variables-and-mutability.md) это даст ссылки на один и тот же объект:
matrix = [[0] * 3] * 3
matrix[0][0] = 99
print(matrix)   # [[99, 0, 0], [99, 0, 0], [99, 0, 0]] - все строки одно и то же!

Правильно через comprehension:

matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 99
print(matrix)   # [[99, 0, 0], [0, 0, 0], [0, 0, 0]]

Доступ и модификация

nums = [10, 20, 30, 40, 50]

# Чтение по индексу
nums[0]      # 10
nums[-1]     # 50 - последний
nums[-2]     # 40 - предпоследний

# Запись
nums[0] = 100
nums.append(60)         # добавить в конец
nums.insert(0, 5)       # вставить по индексу
nums.extend([70, 80])   # добавить элементы из iterable

# Удаление
del nums[0]             # по индексу
nums.remove(30)         # первое вхождение значения
last = nums.pop()       # последний + удалить
first = nums.pop(0)     # по индексу + удалить

nums[100] для несуществующего индекса бросает IndexError. Проверь длину или используй try/except.

Срезы (slice)

list[start:stop:step] - один из самых мощных инструментов:

nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

nums[2:5]       # [2, 3, 4]
nums[:3]        # [0, 1, 2] - с начала
nums[7:]        # [7, 8, 9] - до конца
nums[:]         # копия всего списка
nums[::2]       # [0, 2, 4, 6, 8] - каждый второй
nums[::-1]      # [9, 8, ..., 0] - реверс
nums[1:8:2]     # [1, 3, 5, 7]

Стандарт: [start:stop:step]. Все три параметра опциональны. stop не включается - полу-открытый интервал.

Срезы создают копию, не view (в отличие от NumPy):

a = [1, 2, 3, 4, 5]
b = a[1:4]
b[0] = 999
print(a)   # [1, 2, 3, 4, 5] - не изменился
print(b)   # [999, 3, 4]

Slice assignment

Срезы работают и для присваивания:

nums = [1, 2, 3, 4, 5]

nums[1:3] = [20, 30]        # заменить
nums[1:3] = [20, 30, 40]    # заменить большим - длина списка изменится
nums[1:3] = []              # удалить участок
nums[:] = [100, 200]        # заменить всё содержимое (но сам объект тот же)

nums[:] = ... - идиома для модификации списка in-place, не пересоздавая. Полезно когда есть другие ссылки на этот список.

Поиск и подсчёт

nums = [1, 2, 3, 2, 4, 2]

2 in nums          # True
nums.index(2)      # 1 - первое вхождение
nums.index(2, 2)   # 3 - первое вхождение начиная с индекса 2
nums.count(2)      # 3 - сколько раз встречается

index бросает ValueError если не нашёл. Безопаснее try/except или проверка через in.

Сортировка

Два способа:

nums = [3, 1, 4, 1, 5, 9, 2, 6]

# 1. sorted() - возвращает новый список
sorted_nums = sorted(nums)
print(nums)        # [3, 1, 4, ...] - не изменился
print(sorted_nums) # [1, 1, 2, 3, 4, 5, 6, 9]

# 2. list.sort() - модифицирует на месте, возвращает None
nums.sort()
print(nums)        # [1, 1, 2, 3, 4, 5, 6, 9]

Параметры:

sorted(nums, reverse=True)   # по убыванию

users = [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]

# По ключу
sorted(users, key=lambda u: u["age"])
sorted(users, key=lambda u: u["name"])

# По нескольким полям (через tuple)
sorted(users, key=lambda u: (u["age"], u["name"]))

# Stable: при равных ключах сохраняет порядок исходного

Ключ сортировки удобно задавать лямбдой или operator.itemgetter. Python использует Timsort - стабильный алгоритм со сложностью O(n log n). При повторных сортировках по разным критериям можно сортировать сначала по второстепенному, потом по главному.

reverse - обратный порядок

nums = [1, 2, 3, 4]

# reverse() - на месте
nums.reverse()
print(nums)   # [4, 3, 2, 1]

# reversed() - возвращает iterator
for x in reversed([1, 2, 3]):
    print(x)   # 3, 2, 1

# Через срез - копия
nums[::-1]   # [4, 3, 2, 1] - но создаёт новый список

reversed() - ленивая, не создаёт копию. Полезно для прохода без аллокации.

Объединение и проверки

# Конкатенация
[1, 2] + [3, 4]   # [1, 2, 3, 4]

# Повторение
[1, 2] * 3        # [1, 2, 1, 2, 1, 2]

# Длина
len([1, 2, 3])    # 3

# Сумма/min/max
sum([1, 2, 3])    # 6
min([3, 1, 4])    # 1
max([3, 1, 4])    # 4

# all / any
all([True, True, True])    # True
any([False, False, True])  # True

# Проверка пустоты
if not items:
    print("пустой")

if items:
    print("есть элементы")

Эффективность операций

ОперацияСложность
lst[i]O(1)
lst[i:j]O(j - i)
lst.append(x)O(1) амортизированная
lst.insert(0, x)O(n) - сдвиг всех
lst.pop()O(1)
lst.pop(0)O(n) - сдвиг всех
x in lstO(n) - линейный поиск
lst.sort()O(n log n)
len(lst)O(1)

Запомни: вставки и удаления в начало дорогие. Если часто работаешь с обоими концами - используй collections.deque.

Распаковка

# Базовая
a, b, c = [1, 2, 3]

# С * для остатка
first, *rest = [1, 2, 3, 4]      # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4]       # init=[1,2,3], last=4
first, *mid, last = [1, 2, 3, 4] # first=1, mid=[2,3], last=4

# Пропуск
_, second, _ = [1, 2, 3]

# В вложенном виде
((a, b), c) = ((1, 2), 3)

*rest собирает остаток в список. _ - конвенция для «не интересует».

Список как стек/очередь

Стек (LIFO):

stack = []
stack.append(1)   # push
stack.append(2)
stack.append(3)
top = stack.pop() # pop - последний

Очередь (FIFO) - не используй list, медленно:

queue = []
queue.append(1)   # enqueue
queue.append(2)
first = queue.pop(0)   # dequeue - O(n)!

Для очереди используй collections.deque, а про сами структуры «стек и очередь» есть урок в треке алгоритмов.

Идиомы

Фильтрация:

positives = [x for x in nums if x > 0]
# или
positives = list(filter(lambda x: x > 0, nums))

Синтаксис [... for ... in ...] разбирали в уроке про comprehensions.

Преобразование:

doubled = [x * 2 for x in nums]
# или
doubled = list(map(lambda x: x * 2, nums))

Уникальные с сохранением порядка:

# Через dict (упорядоченный с Python 3.7)
unique = list(dict.fromkeys(nums))

# Через set - порядок не сохраняется
unique_unordered = list(set(nums))

Группировка по ключу:

from collections import defaultdict

users = [{"role": "admin", "name": "A"}, {"role": "user", "name": "B"}]
by_role = defaultdict(list)
for u in users:
    by_role[u["role"]].append(u["name"])

Распространённые ошибки

1. Модификация во время итерации (повтор):

nums = [1, 2, 3, 4, 5]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)   # baddrum

# Правильно
nums = [n for n in nums if n % 2 != 0]

2. Изменение по shared reference:

default = []
a = default
b = default
a.append(1)
# b тоже [1] - они один объект

3. Сравнение sort() с возвращаемым значением:

# sort возвращает None, не отсортированный список!
result = nums.sort()   # result is None
# Используй sorted() если нужен возврат
result = sorted(nums)

Сравнение с Go и PHP

В Go slice похож на Python list, но с типизацией и без удобных srезов с шагом:

nums := []int{1, 2, 3, 4, 5}
sub := nums[1:3]    // [2, 3]
nums = append(nums, 6)

В PHP массивы - универсальные структуры (одновременно list и dict), что даёт гибкость, но усложняет понимание:

$arr = [1, 2, 3];
$arr[] = 4;       // добавить
array_push($arr, 5);

Python list более похож на Go slice по поведению, но более выразительный за счёт slice-нотации.

Мини-задание

  1. Срезы и реверс:
nums = list(range(10))   # [0, 1, ..., 9]

print(nums[::-1])        # реверс
print(nums[::2])         # каждый второй
print(nums[-3:])         # последние три
print(nums[3:7])         # с индекса 3 до 6
  1. Сортировка по нескольким ключам:
people = [
    {"name": "Alice", "age": 30, "city": "Moscow"},
    {"name": "Bob", "age": 25, "city": "London"},
    {"name": "Charlie", "age": 30, "city": "Paris"},
]

# Сортировка по age, при равном - по name
sorted_people = sorted(people, key=lambda p: (p["age"], p["name"]))
for p in sorted_people:
    print(p)
  1. Уникальные с сохранением порядка:
items = [1, 2, 3, 2, 4, 1, 5, 3]
unique = list(dict.fromkeys(items))
print(unique)   # [1, 2, 3, 4, 5]

Что дальше

Освоили списки. В следующем уроке - кортежи и NamedTuple: иммутабельные последовательности и их типизированные версии.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.