💻 Примеры кода

⚡ Список одной строкой, стек и очередь без лишнего кода

from collections import deque
nums = [1, 2, 3, 4, 5]
print([n ** 2 for n in nums if n % 2 == 0])   # [4, 16]  LC с фильтром
stack = []; stack.append(1); stack.append(2)
print(stack.pop())                            # 2        LIFO: append/pop
queue = deque([1, 2, 3])
print(queue.popleft())                        # 1        FIFO: popleft() — O(1)
КонструкцияЧто делаетВозвращает
[expr for x in it if cond]список: expr — что оставить, cond — фильтр после forновый список
[a if cond else b for x in it]выбор значения для каждого элемента, if...else — до forновый список той же длины
list.append(x) / list.pop()стек (LIFO): добавить и снять верхний элементO(1) для обоих
deque.append(x) / deque.popleft()очередь (FIFO): добавить в конец, снять с началаO(1) — в отличие от list.pop(0)
Топ-3 ошибки: путать ссылку и копию · ожидать порядок там, где его нет (set, разная длина zip) · усложнять выражение, когда обычный цикл читается лучше.

Примеры идут от простого к сложному: сначала базовый list comprehension и его варианты, затем сравнение с циклом и с map/filter, затем стек и очередь — сначала на обычном списке, затем на collections.deque с измерением реальной разницы в скорости. Весь вывод в комментариях получен запуском кода, а не набран по памяти.

Пример 1. Базовый list comprehension и его цикл-эквивалент

List comprehension строит новый список из итерируемого объекта в одной строке. Исходная коллекция не меняется, если в выражении нет побочных эффектов.

numbers = [1, 4, 6, 7, 9]

squares_loop = []
for n in numbers:
    squares_loop.append(n ** 2)

squares_lc = [n ** 2 for n in numbers]

print(squares_loop)  # [1, 16, 36, 49, 81]
print(squares_lc)    # [1, 16, 36, 49, 81]
print(numbers)        # [1, 4, 6, 7, 9] — исходный список не тронут

Что происходит: обе записи делают одно и то же — берут каждый n из numbers и кладут n ** 2 в новый список. Comprehension — не новая возможность языка, а более компактная запись того же цикла с append.

Пример 2. Фильтр if — только часть элементов

if в конце comprehension — это фильтр: элемент попадёт в результат, только если условие истинно. Место фильтра — строго после for.

even_numbers = [number for number in range(10) if number % 2 == 0]
print(even_numbers)  # [0, 2, 4, 6, 8]

words = ["cat", "elephant", "dog", "bird", "lion", "ant"]
long_words_reversed = [word[::-1] for word in words if len(word) > 3]
print(long_words_reversed)  # ['tnahpele', 'drib', 'noil']

Что происходит: для каждого слова длиннее трёх символов выражение word[::-1] переворачивает строку срезом; слова короче трёх символов ("cat", "dog", "ant") в результат не попадают вовсе — фильтр отбросил их до вычисления выражения.

Пример 3. Условное выражение if...else — оно стоит перед for

Если нужно не отфильтровать, а выбрать одно из двух значений для каждого элемента, тернарный if...else пишется в начале выражения, до for. Это самое частое место путаницы в теме.

numbers = [2, 7, 5, 4, 1, 1, 7, 8]
modified = [number if number % 2 == 0 else -1 for number in numbers]
print(modified)  # [2, -1, -1, 4, -1, -1, -1, 8]

try:
    eval("[n if n % 2 == 0 for n in range(5)]")
except SyntaxError as error:
    print(f"SyntaxError: {error}")
    # SyntaxError: expected 'else' after 'if' expression (<string>, line 1)

Почему так: в результате остались все восемь элементов — длина списка не изменилась, в отличие от фильтра из примера 2. Вторая попытка показывает типичную ошибку: if без else после for Python принимает как незаконченное тройное выражение и требует else, а не трактует его как фильтр.

Пример 4. Вложенный comprehension: разворот и обработка матрицы

Вложенный comprehension читается как обычные вложенные циклы — слева направо: во внешнем цикле сначала идёт строка, потом элемент строки.

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

flattened = [value for row in matrix for value in row]
row_sums = [sum(row) for row in matrix]
doubled_flat = [value * 2 for row in matrix for value in row]

print(flattened)     # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(row_sums)      # [6, 15, 24]
print(doubled_flat)  # [2, 4, 6, 8, 10, 12, 14, 16, 18]

Что происходит: flattened — это for row in matrix for value in row, то есть «для каждой строки, для каждого значения в строке» — тот же порядок, что у двух вложенных for. row_sums — comprehension верхнего уровня, где элемент — не число, а сама строка, к которой применяется sum().

Пример 5. List comprehension против map/filter

Тот же результат, что comprehension с фильтром из примера 2, можно получить связкой встроенных функций высшего порядка. Comprehension в Python обычно читается яснее — подробный разбор map/filter/lambda в уроке 42.

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

lc_result = [x ** 2 for x in nums if x % 2 == 0]
mf_result = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, nums)))

print(lc_result)               # [4, 16]
print(mf_result)               # [4, 16]
print(lc_result == mf_result)  # True

Что происходит: оба варианта дают одинаковый список, но map/filter читаются изнутри наружу (сначала filter, потом map), а comprehension — слева направо, в порядке «что вычисляем → откуда → при каком условии». Это не значит, что map/filter не нужны: они естественны, когда функция уже готова (map(str, nums)), без обёртки в lambda.

⚠️ Проверить по документации: сравнение с map/filter не входит в материалы этого урока — это дополнение курса. Подробности — в разделе List Comprehensions официальной документации.

Пример 6. zip(): параллельный обход, разная длина, разовый итератор

zip() сопоставляет элементы нескольких последовательностей по позиции. Результат — итератор кортежей: он останавливается на самой короткой последовательности и его нельзя пройти дважды.

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
print(list(zip(names, ages)))
# [('Alice', 25), ('Bob', 30), ('Charlie', 35)]

short = [10, 20, 30]
letters = ["x", "y"]
print(list(zip(short, letters)))
# [(10, 'x'), (20, 'y')] — третий элемент short потерян, ошибки нет

zipped = zip(names, ages)
print(list(zipped))  # [('Alice', 25), ('Bob', 30), ('Charlie', 35)]
print(list(zipped))  # [] — итератор уже исчерпан

Что происходит: для списков разной длины zip тихо обрезает результат по короткому — значение 30 из short нигде не появилось, и Python не предупредил об этом. Второй вызов list(zipped) вернул пустой список: как и map/filter, zip — одноразовый итератор.

Пример 7. Стек (LIFO) на списке: append/pop

Стек — структура LIFO («последним пришёл — первым ушёл»). На обычном списке она реализуется двумя методами: append() кладёт элемент наверх, pop() без аргумента снимает и возвращает верхний.

stack = []
stack.append(1)
stack.append(2)
stack.append(3)
print(stack)  # [1, 2, 3]

top = stack.pop()
print("Сняли:", top)          # Сняли: 3
print("Стек:", stack)         # Стек: [1, 2]
print("Верхний:", stack[-1])  # Верхний: 2


def check_parentheses(s):
    balance_stack = []
    for char in s:
        if char == "(":
            balance_stack.append(char)
        elif char == ")":
            if not balance_stack:
                return False
            balance_stack.pop()
    return not balance_stack


print(check_parentheses("(())"))  # True
print(check_parentheses("(()"))   # False
print(check_parentheses("())("))  # False

Что происходит: append/pop у списка работают с концом списка за O(1) — ни один элемент не сдвигается. Проверка скобок — классическая задача на стек: открывающая скобка кладётся на стек, закрывающая снимает последнюю открытую; если снимать нечего или стек не пуст в конце — баланс нарушен.

Пример 8. Очередь (FIFO) на списке — и почему это медленно

Очередь — структура FIFO («первым пришёл — первым ушёл»). На списке она тоже реализуема, но pop(0) удаляет с начала, и после каждого удаления все оставшиеся элементы сдвигаются на одну позицию влево — это O(n), а не O(1).

import time

queue = []
queue.append(1)
queue.append(2)
queue.append(3)
print(queue)  # [1, 2, 3]

first = queue.pop(0)
print("Обслужили:", first)  # Обслужили: 1
print("Очередь:", queue)    # Очередь: [2, 3]
print("Первый:", queue[0])  # Первый: 2

n = 20000
lst = list(range(n))
start = time.perf_counter()
while lst:
    lst.pop(0)
list_time = time.perf_counter() - start
print(f"pop(0) для {n} элементов: {list_time:.4f} c")
# pop(0) для 20000 элементов: 0.0388 c

Что происходит: функционально всё работает верно, но на 20 000 элементах опустошение списка через pop(0) заняло заметное время именно из-за постоянных сдвигов — это и есть цена O(n)-операции, повторённой n раз.

Пример 9. Очередь на collections.deque: popleft() и реальная разница в скорости

deque («double-ended queue») из collections — двусторонняя очередь: добавление и удаление с обоих концов работают за O(1). Для очереди это правильный инструмент, а не список.

from collections import deque

queue2 = deque(["print", "scan", "send"])
print(queue2.popleft())  # print

dq = deque(range(n))
start = time.perf_counter()
while dq:
    dq.popleft()
deque_time = time.perf_counter() - start
print(f"popleft() для {n} элементов: {deque_time:.4f} c")
print(f"deque быстрее в {list_time / deque_time:.1f} раз")
# popleft() для 20000 элементов: 0.0017 c
# deque быстрее в 23.4 раз

Что происходит: deque хранит элементы блоками в двусвязном списке, поэтому удаление с начала не требует сдвига остальных — на том же объёме данных popleft() оказался на порядок быстрее pop(0). Точное число раз зависит от машины и версии Python — важен порядок величины, а не конкретная цифра.

⚠️ Проверить по документации: гарантии сложности O(1) для deque.append()/deque.popleft() против O(n) для list.pop(0) сверьте по collections.deque и TimeComplexity.

Пример 10. Устойчивая сортировка: sorted(key=...)

Сортировка в Python устойчива: элементы с одинаковым значением ключа сохраняют исходный взаимный порядок. Это не побочный эффект, а гарантия языка.

words = ["apple", "dog", "bat", "cat", "banana"]
print(sorted(words, key=len))
# ['dog', 'bat', 'cat', 'apple', 'banana']
# dog/bat/cat — длина 3, и они остались в исходном порядке друг относительно друга

more_words = ["orange", "mango", "apple", "banana", "kiwi", "cherry"]
for w in sorted(more_words, key=len):
    print(f"{len(w)}: {w}")
# 4: kiwi
# 5: mango
# 5: apple
# 6: orange
# 6: banana
# 6: cherry

data = [("a", 2), ("b", 1), ("c", 2), ("d", 1)]
print(sorted(data, key=lambda pair: pair[1]))
# [('b', 1), ('d', 1), ('a', 2), ('c', 2)]

Почему так: среди слов длиной 5 порядок остался mango, apple — так же, как в исходном списке, хотя сортировка шла только по len(w). То же с парами: у ключей 1 порядок остался "b", "d", у ключей 2 — "a", "c". Устойчивость — то, что позволяет сортировать по нескольким критериям несколькими последовательными вызовами sorted().

Пример 11. Стек, очередь и comprehension вместе: кольцевой буфер событий

На практике эти инструменты комбинируются. deque(maxlen=N) — готовый кольцевой буфер: при заполнении новый элемент вытесняет самый старый, вручную ничего удалять не нужно. Comprehension здесь же отбирает важные события из полной истории.

events = ["login", "click", "logout", "click", "error"]

recent = deque(maxlen=3)
for e in events:
    recent.append(e)
    print(list(recent))
# ['login']
# ['login', 'click']
# ['login', 'click', 'logout']
# ['click', 'logout', 'click']
# ['logout', 'click', 'error']

urgent = [e for e in events if e in ("error", "logout")]
print(urgent)  # ['logout', 'error']

Что происходит: как только в recent оказывается четвёртый элемент, deque сам выбрасывает самый старый — буфер держит ровно последние 3 события без ручного pop(0) или проверки длины. urgent при этом прошёл по полному списку событий (не по буферу) и отобрал только два важных — независимая задача, решённая обычным comprehension с фильтром из примера 2.

⚠️ Проверить по документации: параметр maxlen у deque не упоминался в материалах урока — проверьте поведение по collections.deque.

Что дальше

Разберите файлы репозитория — там те же идеи в исходном виде из лекции, затем переходите к заданиям. За методами списка, которые пригодятся для стека, — в урок 26; за остальными структурами модуля collections (Counter, defaultdict, OrderedDict) — в урок 40. Если что-то из примеров не запустилось — сначала загляните в типичные ошибки.