Перейти к содержимому

Работа с последовательностями в Python [2.5]

Работа с последовательностями в Python [2.5]

В этом уроке разберём, как Python перебирает элементы коллекций: итерируемые объекты, итераторы, генераторы, а также компактный синтаксис: list/dict comprehensions и generator expressions. В конце — базовые алгоритмы поиска и сортировки на последовательностях.

Главная мысль: цикл for — это “вежливый интерфейс” над протоколом итератора. Если понять iter(), next() и StopIteration, многие вещи в Python станут логичными.

Содержание

1. Итераторы и итерируемые объекты

Что такое iterable и iterator

Вы уже знаете структуры данных, которые содержат много элементов: списки, кортежи, словари, множества, строки. Такие объекты можно “перебирать” — они итерируемые (англ. iterable).

Простое различие:
  • Iterable — объект, по которому можно начать перебор (например, список).
  • Iterator — объект “текущего состояния перебора”, который умеет отдавать следующий элемент.

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

Протокол итератора: __iter__() и __next__()

В Python итерация построена на двух методах:

  • __iter__() — возвращает итератор (объект, который будет отдавать элементы).
  • __next__() — возвращает следующий элемент; когда элементы закончились — вызывает исключение StopIteration.

Как работает for “под капотом”

Когда вы пишете цикл:

array = [1, 2, 3]
for a in array:
    pass

Python делает примерно следующее (упрощённо):

array = [1, 2, 3]
it = array.__iter__()     # или iter(array)

while True:
    try:
        a = it.__next__() # или next(it)
    except StopIteration:
        break
    # тело цикла for:
    pass
Ключевая идея: цикл for останавливается именно потому, что в конце итератор вызывает StopIteration.

iter() и next(): “человеческие” функции вместо магических методов

Вы можете работать с итераторами напрямую через встроенные функции iter() и next().

array = (1, 2, 3)
it = iter(array)      # создаём итератор

print(next(it))       # 1
print(next(it))       # 2

StopIteration: что происходит на конце последовательности

Если вызвать next() больше раз, чем элементов в итераторе, будет исключение StopIteration.

it = iter([1, 2, 3])

print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
print(next(it))  # StopIteration
Практический лайфхак: у next() есть второй аргумент — значение по умолчанию. Тогда исключение не будет выброшено.
it = iter([1, 2, 3])

print(next(it, None))  # 1
print(next(it, None))  # 2
print(next(it, None))  # 3
print(next(it, None))  # None (вместо StopIteration)

Типичные ошибки с итераторами (с разбором)

Ошибка 1. Путать iterable и iterator

Список — iterable. Итератор списка — iterator. Если вы уже получили итератор и “проели” его, повторить перебор не получится.

nums = [1, 2, 3]
it = iter(nums)

print(list(it))  # [1, 2, 3]
print(list(it))  # []  итератор уже закончился
Как правильно: либо снова создать итератор it = iter(nums), либо сразу сохранить результат, если он нужен несколько раз.

Ошибка 2. Вызывать next() на iterable без iter()

next() работает с итератором, а не со списком напрямую. (Список сам по себе не обязан иметь __next__().)

nums = [1, 2, 3]
# next(nums)   # TypeError: 'list' object is not an iterator

it = iter(nums)
print(next(it)) # 1

Ошибка 3. Менять список во время обхода

Если удалять/вставлять элементы в список во время перебора, можно пропустить элементы или получить неожиданный результат.

arr = [1, 2, 3, 4, 5]
for x in arr:
    if x % 2 == 0:
        arr.remove(x)

print(arr)
# часто неожиданно: [1, 3, 5] (или иной эффект на других кейсах)
Правильнее: создавать новый список через фильтрацию.
arr = [1, 2, 3, 4, 5]
arr2 = [x for x in arr if x % 2 != 0]
print(arr2)  # [1, 3, 5]
↑ К оглавлению

2. Генераторы и ленивые вычисления

Зачем нужны генераторы

Генератор — это удобный способ создавать итераторы “на лету”. Он особенно полезен для больших последовательностей: значения вычисляются по мере необходимости, а не сохраняются сразу целиком в памяти. Это называют ленивыми вычислениями.

yield vs return: в чём разница

В обычной функции return завершает выполнение и возвращает значение. В генераторе yield “выдаёт” значение и приостанавливает функцию, чтобы продолжить с того же места при следующем next().

Запомнить легко:
  • return — “вернул и закончился”.
  • yield — “отдал порцию и запомнил место”.

Пример: генератор факториалов

def factorial(N):
    a = 1
    for i in range(1, N + 1):
        a *= i
        yield a

my_gen = factorial(4)

print(next(my_gen))  # 1
print(next(my_gen))  # 2
print(next(my_gen))  # 6
print(next(my_gen))  # 24

Каждое значение вычисляется только в момент вызова next().

Почему range — не список

range(start, end, step) ведёт себя как “последовательность чисел”, но обычно не хранит все числа в памяти сразу. Он выдаёт значения по мере перебора, то есть работает лениво (как итератор/генераторная последовательность).

r = range(1, 10, 2)

for x in r:
    print(x)
# 1
# 3
# 5
# 7
# 9

Типичные ошибки с генераторами (с разбором)

Ошибка 1. Ожидать, что генератор можно “пройти” два раза

Генератор (как и итератор) одноразовый: после полного прохода он “заканчивается”.

gen = (x**2 for x in range(3))

print(list(gen))  # [0, 1, 4]
print(list(gen))  # [] генератор исчерпан
Решение: если нужен повторный проход — пересоздавайте генератор или сохраняйте результаты в список.

Ошибка 2. “Случайно съесть” генератор через list()

Любое преобразование в list() полностью перебирает генератор и забирает все элементы. После этого генератор пуст.

Ошибка 3. Использовать return внутри генератора как “ещё один yield”

return внутри генератора завершает генератор. Это нормально, но важно понимать смысл: “значений больше не будет”.

↑ К оглавлению

3. Списковые включения (list comprehensions)

Базовый синтаксис

Списковое включение — это компактный способ создать новый список на основе существующего итерируемого объекта.

numbers = [1, 2, 3, 4, 5]
squared_numbers = [x**2 for x in numbers]
print(squared_numbers)
# [1, 4, 9, 16, 25]
Общий вид: new_list = [expression for item in iterable if condition]
Часть if condition — необязательная.

Условие if внутри включения (как filter)

Можно оставить только элементы, подходящие под условие — так же, как делает filter().

numbers = [1, 2, 3, 4, 5]
squared_even = [x**2 for x in numbers if x % 2 == 0]
print(squared_even)
# [4, 16]

Вложенные включения (как двойной цикл)

Если нужно создать комбинации “каждый с каждым”, можно вложить два цикла в одно включение.

list1 = [1, 2, 3]
list2 = [3, 4, 5]

new_list = [(x, y) for x in list1 for y in list2 if x + y == 6]
print(new_list)
# [(1, 5), (2, 4)]

Это эквивалентно вложенным циклам for + условие if.

Сравнение: for+append, map(), comprehension

Одна и та же задача “квадраты чисел” тремя способами:

ПодходПримерКогда удобен
Классический цикл for + append Когда логика сложнее, чем 1 выражение, или есть побочные действия
Функциональный стиль list(map(lambda x: ..., seq)) Когда функцию удобно передать как объект (или уже есть готовая функция)
Списковое включение [expr for x in seq if cond] Когда нужно быстро и компактно создать список (часто читается лучше)

Типичные ошибки списковых включений (и когда лучше не использовать)

Ошибка 1. Использовать включение ради “действия”, а не результата

Списковое включение должно создавать список. Если вы используете его ради print или других side effects — получится мусор (список из None) и плохая читаемость.

# Плохо: список не нужен, а побочные эффекты есть
res = [print(x) for x in [1, 2, 3]]
print(res)
# 1
# 2
# 3
# [None, None, None]
Как правильно: используйте обычный for, если цель — действие.

Ошибка 2. Слишком сложные вложенные включения (падает читаемость)

Если включение содержит несколько for, несколько условий и сложное выражение — часто выгоднее развернуть в обычный код, чтобы было проще поддерживать.

↑ К оглавлению

4. Генераторы структур: dict и generator expressions

Dictionary comprehension (генератор словарей)

Словари тоже можно создавать компактно. Формула: {key_expression: value_expression for element in iterable if condition}.

my_dict = {x: x**2 for x in range(5)}
print(my_dict)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Словарь из двух списков через zip()

Частый практический приём: есть список ключей и список значений — “склеиваем” их через zip и строим словарь.

keys = ['a', 'b', 'c']
values = [1, 2, 3]

new_dict = {k: v for k, v in zip(keys, values)}
print(new_dict)
# {'a': 1, 'b': 2, 'c': 3}

Generator expression: “генераторное включение”

Если вместо списка вам нужен ленивый поток значений, используйте круглые скобки:

my_generator = (x**2 for x in range(5))

for value in my_generator:
    print(value)
# 0
# 1
# 4
# 9
# 16
Разница скобок:
  • [ ... ] — создаёт список сразу (в памяти).
  • { ... } — создаёт словарь или множество (в зависимости от синтаксиса).
  • ( ... ) — создаёт генератор (лениво).

Типичные ошибки dict/gen включений

Ошибка 1. Дубликаты ключей в dict comprehension

Если выражение ключа даёт одинаковые ключи, более поздние значения перезапишут ранние. Это не ошибка Python — это свойство словаря.

data = [1, 2, 3, 4]
d = {x % 2: x for x in data}
print(d)
# {1: 3, 0: 4}  (1 перезаписался, 0 перезаписался)

Ошибка 2. Не понимать, что generator expression одноразовый

Генераторные выражения — это итераторы. После перебора они заканчиваются (как и любые генераторы).

↑ К оглавлению

5. Алгоритмы на последовательностях: поиск и сортировка

Линейный поиск

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

def linear_search(arr, x):
    for i, a in enumerate(arr):
        if a == x:
            return i
    return None
Плюс: работает на любом списке, сортировка не нужна.
Минус: на больших списках может быть медленно.

Бинарный поиск

Бинарный поиск работает только на отсортированном массиве. Он каждый раз сравнивает с серединой и отбрасывает половину элементов.

def binary_search(arr, x):
    low = 0
    high = len(arr) - 1

    while low <= high:
        mid = (high + low) // 2

        if arr[mid] < x:
            low = mid + 1
        elif arr[mid] > x:
            high = mid - 1
        else:
            return mid

    return None
Важно: если массив не отсортирован, бинарный поиск может вернуть неправильный ответ (или не найти элемент).

Сортировки: пузырёк, выбор, вставки

Сортировка пузырьком (bubble sort)

Сравнивает соседние элементы и меняет местами, если порядок неправильный. Повторяет проходы, пока всё не отсортируется.

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

Сортировка выбором (selection sort)

На каждом шаге ищет минимальный элемент в неотсортированной части и ставит его в “следующую” позицию отсортированной части.

def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr

Сортировка вставками (insertion sort)

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

def insertion_sort(arr):
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

“Разделяй и властвуй”: quick sort и merge sort

Быстрая сортировка (quick sort)

Выбирает опорный элемент (pivot), разбивает массив на элементы меньше/больше и рекурсивно сортирует части.

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[0]
    less = [x for x in arr[1:] if x <= pivot]
    greater = [x for x in arr[1:] if x > pivot]
    return quick_sort(less) + [pivot] + quick_sort(greater)
Важная деталь: этот вариант создаёт новые списки less/greater, то есть расходует память. Это нормально для обучения, но в реальных задачах обычно используют встроенную сортировку Python.

Сортировка слиянием (merge sort)

Рекурсивно делит массив пополам до одноэлементных списков, а затем аккуратно “сливает” их обратно в правильном порядке.

def merge_sort(arr):
    if len(arr) > 1:
        mid = len(arr) // 2
        L = arr[:mid]
        R = arr[mid:]

        merge_sort(L)
        merge_sort(R)

        i = j = k = 0

        while i < len(L) and j < len(R):
            if L[i] < R[j]:
                arr[k] = L[i]
                i += 1
            else:
                arr[k] = R[j]
                j += 1
            k += 1

        while i < len(L):
            arr[k] = L[i]
            i += 1
            k += 1

        while j < len(R):
            arr[k] = R[j]
            j += 1
            k += 1

    return arr
Почему merge sort “любит память”: он создаёт подсписки L и R. Это делает алгоритм понятным, но расходует дополнительную память.

Таблица: скорость/память/когда применять

АлгоритмИдеяОценка по времени (в среднем)Доп. памятьКомментарий
Линейный поиск Перебор O(n) O(1) Работает всегда, но медленно на больших данных
Бинарный поиск Половинное деление O(log n) O(1) Только для отсортированных массивов
Bubble sort Соседние обмены O(n²) O(1) Простой, но почти всегда слишком медленный
Selection sort Выбор минимума O(n²) O(1) Тоже медленный, но понятный
Insertion sort Вставка в отсортированную часть O(n²) O(1) Хорош на почти отсортированных данных и маленьких массивах
Quick sort Разделяй и властвуй O(n log n) зависит от реализации Быстрый на практике, но чувствителен к выбору pivot
Merge sort Разделяй и властвуй + слияние O(n log n) O(n) Стабильный, предсказуемый, но требует память

Как сортирует Python (TimSort) — практический вывод

В реальном коде почти всегда используйте встроенные инструменты: sorted(iterable) (возвращает новый список) и list.sort() (сортирует список на месте). Внутри Python применяет гибридный алгоритм (TimSort), который обычно очень эффективен на “живых” данных.

Практика: писать свои сортировки полезно для обучения, но в продакшене чаще лучше доверять sorted()/sort().

Типичные ошибки в алгоритмах (с разбором)

Ошибка 1. Запуск бинарного поиска на неотсортированном массиве

Бинарный поиск предполагает порядок. Если данных порядок не соблюдают — логика “отбрасывания половины” ломается.

Ошибка 2. Путать “сортирует на месте” и “возвращает новый список”

list.sort() возвращает None, потому что сортирует список на месте. А sorted() возвращает новый список.

arr = [3, 1, 2]

x = arr.sort()
print(x)    # None
print(arr)  # [1, 2, 3]

arr2 = sorted([3, 1, 2])
print(arr2) # [1, 2, 3]
↑ К оглавлению

6. Мини‑тесты по теме + разбор

Вопрос 1

Какой метод нужно вызвать у объекта, чтобы получить итератор?

  • __iter__()
  • __next__()
  • __getitem__()
  • __setitem__()
Разбор: __iter__() возвращает итератор. Аналог — iter(obj).

Вопрос 2

Какой метод нужно вызывать у итератора, чтобы получить следующий элемент?

  • __iter__()
  • __next__()
  • __getitem__()
  • __setitem__()
Разбор: __next__() (или next(it)) выдаёт следующий элемент.

Вопрос 3

Что происходит, когда итератор достигает конца последовательности?

  • Итератор начинает обход заново
  • Итератор возвращает пустой объект
  • Итератор перестаёт работать
  • Возникает исключение StopIteration
Разбор: StopIteration — сигнал “элементов больше нет”. Цикл for ловит его и завершает обход.

Вопрос 4

Какой оператор используют для создания генератора?

  • for
  • while
  • yield
  • return
Разбор: генератор “выдаёт” значения через yield.

Вопрос 5

Что такое списковое включение?

  • Функция, которая добавляет элементы в список
  • Способ создания нового списка на основе существующего
  • Конструкция, которая добавляет элементы без циклов и условий
  • Метод, который удаляет элементы из списка
Разбор: это синтаксис, который включает цикл (и иногда условие), но записан компактно.

Вопрос 6

Результат кода:

new_list = [y + x for x in ['a','b','c'] for y in ['d','e','f']]
print(new_list)

Ответ: ['da', 'ea', 'fa', 'db', 'eb', 'fb', 'dc', 'ec', 'fc']

Сначала фиксируется x='a', а y пробегает d,e,f, затем x='b' и снова y.

Вопрос 7

Какая будет сумма элементов списка?

lst = [x for x in range(10) if x % 2 == 0 or x % 3 == 0]
print(lst)
# ?

Правильный список: [0, 2, 3, 4, 6, 8, 9]

Правильная сумма: 0 + 2 + 3 + 4 + 6 + 8 + 9 = 32

Разбор: условие “кратно 2 ИЛИ кратно 3” — поэтому попадают числа, делящиеся хотя бы на одно из них.

Вопрос 8

Что такое линейный поиск?

  • Сравнение среднего элемента
  • Случайный выбор элементов
  • Деление массива пополам
  • Последовательное сравнение каждого элемента с искомым

Вопрос 9

Какой алгоритм поиска можно использовать только для отсортированных массивов?

  • Линейный поиск
  • Бинарный поиск
  • Оба алгоритма
  • Ни один
Разбор: бинарный поиск полагается на порядок элементов.

Вопрос 10

Какая цель у алгоритмов сортировки?

  • Искать элемент
  • Находить максимум
  • Упорядочивать элементы
  • Вычислять среднее

Вопрос 11

Какие сортировки используют принцип “разделяй и властвуй”?

  • Пузырёк
  • Выбором
  • Слиянием
  • Вставками
  • Быстрая сортировка
Разбор: quick sort и merge sort рекурсивно делят массив на части.

Вопрос 12

Какая сортировка использует дополнительную память для промежуточных списков?

  • Пузырьком
  • Выбором
  • Слиянием
  • Быстрая сортировка
Разбор: merge sort хранит отдельные части (L и R) и затем сливает их.
↑ К оглавлению

7. Чек‑лист самопроверки знаний

Отметьте пункты, которые вы действительно понимаете и можете воспроизвести без подсказок.

НавыкПроверка (что я могу сделать)
Отличаю iterable от iterator Могу объяснить, почему список можно перебирать много раз, а итератор — обычно нет
Понимаю протокол итератора Могу объяснить роль __iter__(), __next__() и StopIteration
Умею пользоваться iter() и next() Могу вручную получить 2–3 элемента из коллекции через it = iter(obj) и next(it)
Знаю, что такое генератор Могу написать генератор с yield и получить значения через next()
Понимаю ленивые вычисления Могу объяснить, почему range и генераторы экономят память
Умею писать list comprehensions Могу написать [x**2 for x in nums if x % 2 == 0]
Умею писать dict comprehension Могу создать словарь вида {x: f(x) for x in ...}
Понимаю generator expression Могу создать (x**2 for x in range(10)) и объяснить, почему он одноразовый
Знаю линейный и бинарный поиск Могу объяснить, почему бинарный поиск работает только на отсортированном массиве
Понимаю базовые сортировки Могу словами описать “пузырёк”, “выбор”, “вставки” и их отличия
Понимаю “разделяй и властвуй” Могу объяснить идею quick sort / merge sort на примере “делим на части, потом собираем”
Умею правильно сортировать в Python Могу объяснить разницу sorted() и list.sort()
Вторник, 10 февраля 2026
Работа с последовательностями в Python [2.5]