Python

Модуль 4: Структуры данных

Структуры данных — это способы организации и хранения данных, позволяющие эффективно работать с информацией. В Python есть несколько встроенных структур данных, которые делают язык мощным инструментом для обработки информации.

В этом модуле вы изучите:

  • Списки и кортежи — последовательности для хранения упорядоченных наборов данных
  • Словари — коллекции пар "ключ-значение" для быстрого доступа к данным
  • Множества — неупорядоченные коллекции уникальных элементов
  • Генераторы — мощный механизм для создания последовательностей "на лету"

Понимание структур данных — один из ключевых навыков программиста. Правильный выбор структуры данных может значительно повысить эффективность программы и упростить решение сложных задач.

4.1 Списки и кортежи

Списки и кортежи — это упорядоченные коллекции элементов, которые могут содержать данные разных типов. Они являются одними из самых часто используемых структур данных в Python.

Ключевые отличия:

  • Списки (list) — изменяемые, создаются с помощью квадратных скобок []
  • Кортежи (tuple) — неизменяемые, создаются с помощью круглых скобок ()

Списки (Lists)

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

# Пустой список
пустой_список = []

# Список с элементами
числа = [1, 2, 3, 4, 5]
смешанный_список = [1, "строка", 3.14, True, [1, 2]]

# Создание списка с помощью конструктора list()
список_из_строки = list("Python")  # ['P', 'y', 't', 'h', 'o', 'n']
список_из_диапазона = list(range(5))  # [0, 1, 2, 3, 4]

Доступ к элементам списка

Элементы списка индексируются с 0. Также можно использовать отрицательные индексы для доступа с конца списка.

фрукты = ["яблоко", "банан", "апельсин", "груша", "киви"]

# Доступ по индексу
первый = фрукты[0]  # "яблоко"
последний = фрукты[-1]  # "киви"
предпоследний = фрукты[-2]  # "груша"

# Срезы (slices)
первые_два = фрукты[0:2]  # ["яблоко", "банан"]
с_третьего_до_конца = фрукты[2:]  # ["апельсин", "груша", "киви"]
все_кроме_последних_двух = фрукты[:-2]  # ["яблоко", "банан", "апельсин"]

# Срезы с шагом
каждый_второй = фрукты[::2]  # ["яблоко", "апельсин", "киви"]
в_обратном_порядке = фрукты[::-1]  # ["киви", "груша", "апельсин", "банан", "яблоко"]

Изменение списков

фрукты = ["яблоко", "банан", "апельсин"]

# Изменение элемента
фрукты[1] = "виноград"  # ["яблоко", "виноград", "апельсин"]

# Добавление элементов
фрукты.append("груша")  # Добавляет в конец: ["яблоко", "виноград", "апельсин", "груша"]
фрукты.insert(1, "манго")  # Вставляет по индексу: ["яблоко", "манго", "виноград", "апельсин", "груша"]
фрукты.extend(["киви", "ананас"])  # Добавляет несколько элементов: ["яблоко", "манго", "виноград", "апельсин", "груша", "киви", "ананас"]

# Удаление элементов
удаленный = фрукты.pop()  # Удаляет и возвращает последний элемент: "ананас"
удаленный_по_индексу = фрукты.pop(1)  # Удаляет и возвращает элемент по индексу: "манго"
фрукты.remove("виноград")  # Удаляет первое вхождение элемента
del фрукты[0]  # Удаляет элемент по индексу: ["апельсин", "груша", "киви"]
фрукты.clear()  # Очищает весь список: []

Полезные методы списков

числа = [3, 1, 4, 1, 5, 9, 2, 6]

# Сортировка
числа.sort()  # Сортирует список на месте: [1, 1, 2, 3, 4, 5, 6, 9]
числа.sort(reverse=True)  # Сортирует в обратном порядке: [9, 6, 5, 4, 3, 2, 1, 1]
отсортированный = sorted(числа)  # Возвращает новый отсортированный список

# Обращение списка
числа.reverse()  # Обращает список на месте: [1, 1, 2, 3, 4, 5, 6, 9]

# Подсчёт и поиск
количество = числа.count(1)  # Считает вхождения элемента: 2
индекс = числа.index(5)  # Находит индекс первого вхождения: 4

# Длина списка
длина = len(числа)  # 8

Списки — лучшие практики

  • Используйте списки, когда порядок элементов важен
  • Используйте списки для данных, которые могут изменяться
  • Для больших списков операции вставки/удаления в начале или середине могут быть медленными
  • Списковые включения (list comprehensions) часто более читаемы и эффективны, чем циклы for

Списковые включения (List Comprehensions)

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

# Базовое списковое включение
квадраты = [x**2 for x in range(10)]  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# С условием
четные_квадраты = [x**2 for x in range(10) if x % 2 == 0]  # [0, 4, 16, 36, 64]

# Вложенные циклы
координаты = [(x, y) for x in range(3) for y in range(2)]
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]

# Преобразование данных
имена = ["Анна", "Иван", "Мария", "Петр"]
длины = [len(имя) for имя in имена]  # [4, 4, 5, 4]

# Эквивалент с циклом for
длины = []
for имя in имена:
    длины.append(len(имя))

Кортежи (Tuples)

Создание кортежей

# Пустой кортеж
пустой_кортеж = ()

# Кортеж с элементами
координаты = (10, 20)
точка_3d = (10, 20, 30)

# Кортеж с одним элементом (обязательно с запятой!)
одиночный = (42,)  # Без запятой это будет просто число в скобках

# Создание кортежа без скобок (упаковка кортежа)
имя_возраст = "Иван", 25  # Эквивалентно ("Иван", 25)

# Создание кортежа с помощью конструктора tuple()
кортеж_из_списка = tuple([1, 2, 3])  # (1, 2, 3)
кортеж_из_строки = tuple("Python")  # ('P', 'y', 't', 'h', 'o', 'n')

Доступ к элементам кортежа

Доступ к элементам кортежа осуществляется так же, как и к элементам списка:

координаты = (10, 20, 30, 40, 50)

# Доступ по индексу
x = координаты[0]  # 10
y = координаты[1]  # 20
последний = координаты[-1]  # 50

# Срезы
первые_три = координаты[:3]  # (10, 20, 30)
с_третьего = координаты[2:]  # (30, 40, 50)
каждый_второй = координаты[::2]  # (10, 30, 50)

Распаковка кортежей

Одна из мощных возможностей кортежей — распаковка в отдельные переменные:

# Базовая распаковка
координаты = (10, 20, 30)
x, y, z = координаты  # x = 10, y = 20, z = 30

# Распаковка с игнорированием некоторых значений
имя, _, возраст = ("Иван", "Иванов", 25)  # игнорируем фамилию

# Распаковка с оператором *
первый, *середина, последний = (1, 2, 3, 4, 5)
# первый = 1, середина = [2, 3, 4], последний = 5

# Обмен значениями переменных
a, b = 1, 2
a, b = b, a  # a = 2, b = 1

Неизменяемость кортежей

Кортежи неизменяемы, то есть после создания их нельзя модифицировать:

координаты = (10, 20, 30)

# Следующие операции вызовут ошибку:
# координаты[0] = 100  # TypeError: 'tuple' object does not support item assignment
# координаты.append(40)  # AttributeError: 'tuple' object has no attribute 'append'

# Но можно создать новый кортеж на основе существующего
новые_координаты = координаты + (40, 50)  # (10, 20, 30, 40, 50)
повторенный = координаты * 2  # (10, 20, 30, 10, 20, 30)

Когда использовать кортежи вместо списков?

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

Методы кортежей

Кортежи имеют только два метода, так как они неизменяемы:

числа = (1, 2, 3, 2, 4, 2)

# Подсчёт вхождений элемента
количество_двоек = числа.count(2)  # 3

# Поиск первого вхождения элемента
индекс_тройки = числа.index(3)  # 2

Вложенные структуры

Списки и кортежи могут содержать другие списки и кортежи, создавая многомерные структуры данных:

# Матрица (двумерный список)
матрица = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Доступ к элементам
элемент = матрица[1][2]  # 6 (второй ряд, третий столбец)

# Список кортежей (часто используется для представления записей)
студенты = [
    ("Иван", "Иванов", 20),
    ("Мария", "Петрова", 19),
    ("Алексей", "Сидоров", 21)
]

# Обработка вложенных структур
for имя, фамилия, возраст in студенты:
    print(f"{имя} {фамилия}: {возраст} лет")

Сравнение производительности

Кортежи обычно работают быстрее списков и потребляют меньше памяти. Это особенно заметно при работе с большими объемами данных.

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

Практические примеры

Пример 1: Анализ данных с использованием списков

# Данные о температуре за неделю
температуры = [22, 24, 19, 21, 25, 23, 20]

# Базовый анализ
средняя = sum(температуры) / len(температуры)
максимальная = max(температуры)
минимальная = min(температуры)

print(f"Средняя температура: {средняя:.1f}°C")
print(f"Максимальная температура: {максимальная}°C")
print(f"Минимальная температура: {минимальная}°C")

# Фильтрация данных с помощью спискового включения
выше_среднего = [т for т in температуры if т > средняя]
print(f"Дней с температурой выше средней: {len(выше_среднего)}")

# Индекс самого жаркого дня
самый_жаркий_день = температуры.index(максимальная) + 1  # +1 для перевода из индекса в номер дня
print(f"Самый жаркий день: {самый_жаркий_день}")

Пример 2: Работа с координатами с использованием кортежей

import math

# Список точек на плоскости
точки = [(1, 2), (3, 4), (5, 6), (7, 8)]

# Вычисление расстояний от начала координат
расстояния = []
for x, y in точки:
    расстояние = math.sqrt(x**2 + y**2)
    расстояния.append((x, y, расстояние))

# Сортировка точек по расстоянию
расстояния.sort(key=lambda точка: точка[2])

# Вывод отсортированных точек
print("Точки, отсортированные по расстоянию от начала координат:")
for x, y, расстояние in расстояния:
    print(f"Точка ({x}, {y}): {расстояние:.2f} единиц")

4.2 Словари

Словари (dictionaries) — это неупорядоченные коллекции пар "ключ-значение", обеспечивающие быстрый доступ к данным по ключу. Словари являются одной из самых гибких и мощных структур данных в Python.

Ключевые особенности словарей:

  • Изменяемая структура данных (можно добавлять, изменять и удалять элементы)
  • Доступ к элементам по ключу, а не по индексу
  • Ключи должны быть неизменяемыми (строки, числа, кортежи с неизменяемыми элементами)
  • Значения могут быть любого типа
  • С версии Python 3.7 словари сохраняют порядок добавления элементов

Создание словарей

# Пустой словарь
пустой_словарь = {}
пустой_словарь_2 = dict()

# Словарь с элементами
студент = {
    "имя": "Иван",
    "фамилия": "Петров",
    "возраст": 20,
    "курс": 2,
    "средний_балл": 4.5
}

# Словарь с разными типами ключей и значений
смешанный = {
    "строка": 42,
    10: "число как ключ",
    (1, 2): "кортеж как ключ",
    True: [1, 2, 3]  # список как значение
}

# Создание словаря с помощью конструктора dict()
контакты = dict(Иван="+7-900-123-45-67", Мария="+7-900-765-43-21")

# Создание словаря из списка кортежей
элементы = [("H", "Водород"), ("O", "Кислород"), ("C", "Углерод")]
периодическая_таблица = dict(элементы)

Доступ к элементам словаря

студент = {
    "имя": "Иван",
    "фамилия": "Петров",
    "возраст": 20,
    "курс": 2,
    "средний_балл": 4.5
}

# Доступ по ключу
имя = студент["имя"]  # "Иван"

# Безопасный доступ с методом get()
# Возвращает None, если ключ не существует
отчество = студент.get("отчество")  # None
# Можно указать значение по умолчанию
отчество = студент.get("отчество", "Не указано")  # "Не указано"

# Проверка наличия ключа
if "курс" in студент:
    print(f"Студент на {студент['курс']} курсе")

# Получение всех ключей и значений
ключи = студент.keys()  # dict_keys(['имя', 'фамилия', 'возраст', 'курс', 'средний_балл'])
значения = студент.values()  # dict_values(['Иван', 'Петров', 20, 2, 4.5])

# Получение пар ключ-значение
пары = студент.items()  # dict_items([('имя', 'Иван'), ('фамилия', 'Петров'), ...])

# Перебор словаря
for ключ in студент:
    print(f"{ключ}: {студент[ключ]}")

# Перебор с items() (более эффективный способ)
for ключ, значение in студент.items():
    print(f"{ключ}: {значение}")

Изменение словарей

студент = {"имя": "Иван", "фамилия": "Петров", "возраст": 20}

# Добавление новых элементов
студент["курс"] = 2
студент["факультет"] = "Информатика"

# Изменение существующих элементов
студент["возраст"] = 21

# Добавление нескольких элементов
студент.update({"группа": "И-101", "средний_балл": 4.5})

# Удаление элементов
удаленное_значение = студент.pop("факультет")  # Удаляет и возвращает значение
del студент["группа"]  # Просто удаляет элемент

# Удаление и получение последнего добавленного элемента
последний_элемент = студент.popitem()  # В Python 3.7+ возвращает последний добавленный элемент

# Очистка словаря
студент.clear()  # Удаляет все элементы

Вложенные словари

Словари могут содержать другие словари, создавая иерархические структуры данных:

# Вложенные словари
университет = {
    "факультеты": {
        "информатика": {
            "декан": "Иванов И.И.",
            "кафедры": ["Программирование", "Кибербезопасность", "Сети"]
        },
        "экономика": {
            "декан": "Петров П.П.",
            "кафедры": ["Микроэкономика", "Макроэкономика", "Финансы"]
        }
    },
    "ректор": "Сидоров С.С.",
    "год_основания": 1965
}

# Доступ к вложенным элементам
декан_информатики = университет["факультеты"]["информатика"]["декан"]
кафедры_экономики = университет["факультеты"]["экономика"]["кафедры"]

# Безопасный доступ к вложенным словарям
try:
    кафедры_физики = университет["факультеты"]["физика"]["кафедры"]
except KeyError:
    print("Факультет физики не найден")

# Альтернативный безопасный способ с проверками
физика = университет.get("факультеты", {}).get("физика", {}).get("кафедры", [])

Словарные включения (Dictionary Comprehensions)

Аналогично списковым включениям, словарные включения позволяют компактно создавать словари:

# Создание словаря из списка
имена = ["Анна", "Иван", "Мария", "Петр", "Елена"]
длины_имен = {имя: len(имя) for имя in имена}
# {'Анна': 4, 'Иван': 4, 'Мария': 5, 'Петр': 4, 'Елена': 5}

# С условием
длинные_имена = {имя: len(имя) for имя in имена if len(имя) > 4}
# {'Мария': 5, 'Елена': 5}

# Преобразование пар ключ-значение
оценки = {"математика": 5, "физика": 4, "история": 3}
буквенные_оценки = {
    предмет: "отлично" if оценка == 5 else "хорошо" if оценка == 4 else "удовлетворительно"
    for предмет, оценка in оценки.items()
}
# {'математика': 'отлично', 'физика': 'хорошо', 'история': 'удовлетворительно'}

Полезные методы словарей

# Копирование словаря
оригинал = {"a": 1, "b": 2}
копия = оригинал.copy()  # Создает поверхностную копию
копия["c"] = 3  # Не влияет на оригинал

# Объединение словарей (Python 3.5+)
словарь1 = {"a": 1, "b": 2}
словарь2 = {"b": 3, "c": 4}
объединенный = {**словарь1, **словарь2}  # {'a': 1, 'b': 3, 'c': 4}

# Объединение словарей (Python 3.9+)
объединенный = словарь1 | словарь2  # {'a': 1, 'b': 3, 'c': 4}

# Получение значения с созданием ключа, если его нет
счетчики = {}
слово = "яблоко"
счетчики.setdefault(слово, 0)  # Создаст ключ "яблоко" со значением 0, если его нет
счетчики[слово] += 1  # Теперь безопасно увеличиваем счетчик

# Словарь с значениями по умолчанию
from collections import defaultdict

# Словарь, который по умолчанию создает пустой список для новых ключей
группы = defaultdict(list)
студенты = [("И-101", "Иванов"), ("И-102", "Петров"), ("И-101", "Сидоров")]

for группа, студент in студенты:
    группы[группа].append(студент)  # Не нужно проверять существование ключа

print(группы)  # defaultdict(, {'И-101': ['Иванов', 'Сидоров'], 'И-102': ['Петров']})

Лучшие практики работы со словарями

  • Используйте get() вместо прямого доступа по ключу, если не уверены в наличии ключа
  • Для часто изменяемых словарей используйте collections.defaultdict
  • Для подсчета элементов используйте collections.Counter
  • Помните, что ключи должны быть неизменяемыми
  • Используйте словарные включения для компактного создания словарей

Специализированные словари

В модуле collections есть несколько специализированных типов словарей:

from collections import defaultdict, Counter, OrderedDict

# defaultdict - словарь со значением по умолчанию для новых ключей
int_dict = defaultdict(int)  # По умолчанию будет создавать int() (т.е. 0)
int_dict["a"] += 1  # Не вызовет ошибки, даже если ключа "a" не существовало
print(int_dict)  # defaultdict(, {'a': 1})

# Counter - словарь для подсчета элементов
текст = "абракадабра"
счетчик = Counter(текст)
print(счетчик)  # Counter({'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1})

# Наиболее часто встречающиеся элементы
популярные = счетчик.most_common(2)  # [('а', 5), ('б', 2)]

# OrderedDict - словарь, сохраняющий порядок добавления элементов
# Примечание: с Python 3.7+ обычные словари также сохраняют порядок
упорядоченный = OrderedDict([('c', 3), ('a', 1), ('b', 2)])
print(list(упорядоченный.items()))  # [('c', 3), ('a', 1), ('b', 2)]

Практические примеры

Пример 1: Подсчет частоты слов в тексте

текст = """
Python - высокоуровневый язык программирования общего назначения. 
Python ориентирован на повышение производительности разработчика и 
читаемости кода. Синтаксис Python минималистичен.
"""

# Очищаем текст от знаков препинания и приводим к нижнему регистру
import re
очищенный_текст = re.sub(r'[^\w\s]', '', текст.lower())

# Разбиваем на слова
слова = очищенный_текст.split()

# Подсчитываем частоту слов
частота = {}
for слово in слова:
    частота[слово] = частота.get(слово, 0) + 1

# Сортируем по частоте (от большей к меньшей)
отсортированные = sorted(частота.items(), key=lambda x: x[1], reverse=True)

# Выводим топ-5 самых частых слов
print("Топ-5 самых частых слов:")
for слово, количество in отсортированные[:5]:
    print(f"{слово}: {количество}")

Пример 2: Преобразование данных

# Список студентов с оценками по предметам
студенты = [
    {"имя": "Иван", "математика": 5, "физика": 4, "информатика": 5},
    {"имя": "Мария", "математика": 4, "физика": 5, "информатика": 4},
    {"имя": "Алексей", "математика": 3, "физика": 4, "информатика": 5}
]

# Преобразуем в словарь, где ключ - имя студента
студенты_по_имени = {студент["имя"]: студент for студент in студенты}

# Вычисляем средний балл для каждого студента
for имя, данные in студенты_по_имени.items():
    оценки = [данные[предмет] for предмет in данные if предмет != "имя"]
    средний_балл = sum(оценки) / len(оценки)
    студенты_по_имени[имя]["средний_балл"] = round(средний_балл, 2)

# Создаем словарь средних оценок по предметам
предметы = ["математика", "физика", "информатика"]
средние_по_предметам = {}

for предмет in предметы:
    оценки = [студент[предмет] for студент in студенты]
    средние_по_предметам[предмет] = round(sum(оценки) / len(оценки), 2)

print("Средний балл по студентам:")
for имя, данные in студенты_по_имени.items():
    print(f"{имя}: {данные['средний_балл']}")

print("\nСредний балл по предметам:")
for предмет, средняя in средние_по_предметам.items():
    print(f"{предмет}: {средняя}")

Пример 3: Кэширование результатов функции

def фибоначчи_с_кэшем():
    """Функция для вычисления чисел Фибоначчи с кэшированием."""
    # Создаем словарь для хранения уже вычисленных значений
    кэш = {}
    
    def фиб(n):
        # Если результат уже в кэше, возвращаем его
        if n in кэш:
            return кэш[n]
        
        # Базовые случаи
        if n <= 1:
            результат = n
        else:
            # Рекурсивный случай с использованием кэша
            результат = фиб(n-1) + фиб(n-2)
        
        # Сохраняем результат в кэш
        кэш[n] = результат
        return результат
    
    return фиб

# Создаем функцию с кэшем
фиб = фибоначчи_с_кэшем()

# Теперь вычисление даже больших чисел Фибоначчи будет быстрым
import time

начало = time.time()
результат = фиб(35)
конец = time.time()

print(f"Фибоначчи(35) = {результат}")
print(f"Время выполнения: {(конец - начало):.6f} секунд")

4.3 Множества

Множества (sets) — это неупорядоченные коллекции уникальных элементов. Они идеально подходят для удаления дубликатов, проверки принадлежности элементов и выполнения математических операций над множествами, таких как объединение, пересечение и разность.

Ключевые особенности множеств:

  • Хранят только уникальные элементы (дубликаты автоматически удаляются)
  • Элементы должны быть неизменяемыми (числа, строки, кортежи)
  • Неупорядоченная структура данных (нет индексации)
  • Очень быстрая операция проверки наличия элемента (O(1))
  • Изменяемые (можно добавлять и удалять элементы)
  • Поддерживают математические операции над множествами

Создание множеств

# Пустое множество
пустое_множество = set()  # Нельзя использовать {}, так как это создаст пустой словарь

# Множество с элементами
цвета = {"красный", "зеленый", "синий"}

# Создание множества из других коллекций
список = [1, 2, 2, 3, 3, 3, 4, 5, 5]
уникальные_числа = set(список)  # {1, 2, 3, 4, 5}

строка = "абракадабра"
уникальные_символы = set(строка)  # {'а', 'б', 'р', 'к', 'д'}

Основные операции с множествами

фрукты = {"яблоко", "банан", "апельсин", "груша"}

# Добавление элементов
фрукты.add("киви")  # {"яблоко", "банан", "апельсин", "груша", "киви"}

# Добавление нескольких элементов
фрукты.update(["манго", "ананас"])  # {"яблоко", "банан", "апельсин", "груша", "киви", "манго", "ананас"}

# Удаление элементов
фрукты.remove("банан")  # Вызовет KeyError, если элемента нет
фрукты.discard("виноград")  # Не вызывает ошибку, если элемента нет
удаленный = фрукты.pop()  # Удаляет и возвращает произвольный элемент

# Очистка множества
фрукты.clear()  # Удаляет все элементы

Проверка принадлежности

фрукты = {"яблоко", "банан", "апельсин", "груша"}

# Проверка наличия элемента
if "яблоко" in фрукты:
    print("Яблоко есть в множестве")

if "вишня" not in фрукты:
    print("Вишни нет в множестве")

# Проверка размера множества
размер = len(фрукты)  # 4

Математические операции над множествами

a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# Объединение (элементы, которые есть в A или в B)
объединение = a | b  # {1, 2, 3, 4, 5, 6, 7, 8}
объединение_метод = a.union(b)  # То же самое

# Пересечение (элементы, которые есть и в A, и в B)
пересечение = a & b  # {4, 5}
пересечение_метод = a.intersection(b)  # То же самое

# Разность (элементы, которые есть в A, но нет в B)
разность = a - b  # {1, 2, 3}
разность_метод = a.difference(b)  # То же самое

# Симметрическая разность (элементы, которые есть в A или B, но не в обоих)
симм_разность = a ^ b  # {1, 2, 3, 6, 7, 8}
симм_разность_метод = a.symmetric_difference(b)  # То же самое

Методы сравнения множеств

a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
c = {1, 2, 3}
d = {6, 7, 8}

# Проверка равенства
print(a == c)  # True
print(a == b)  # False

# Проверка, является ли одно множество подмножеством другого
print(a.issubset(b))  # True - все элементы a содержатся в b
print(a <= b)  # То же самое
print(a < b)   # True - строгое подмножество (a подмножество b и a != b)

# Проверка, является ли одно множество надмножеством другого
print(b.issuperset(a))  # True - b содержит все элементы a
print(b >= a)  # То же самое
print(b > a)   # True - строгое надмножество (b надмножество a и a != b)

# Проверка, не пересекаются ли множества
print(a.isdisjoint(d))  # True - нет общих элементов
print(a.isdisjoint(b))  # False - есть общие элементы

Изменение множеств "на месте"

a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# Объединение на месте
a |= b  # a теперь {1, 2, 3, 4, 5, 6, 7, 8}
# или a.update(b)

# Пересечение на месте
a &= {2, 3, 4, 5, 6}  # a теперь {2, 3, 4, 5, 6}
# или a.intersection_update({2, 3, 4, 5, 6})

# Разность на месте
a -= {2, 4, 6}  # a теперь {3, 5}
# или a.difference_update({2, 4, 6})

# Симметрическая разность на месте
a ^= {3, 5, 7}  # a теперь {7}
# или a.symmetric_difference_update({3, 5, 7})

Неизменяемые множества (frozenset)

Python также предоставляет неизменяемую версию множества — frozenset. Она имеет те же методы, что и обычное множество, за исключением методов, изменяющих множество.

# Создание frozenset
неизменяемое = frozenset([1, 2, 3, 4, 5])

# Можно использовать методы, не изменяющие множество
print(неизменяемое.intersection({3, 4, 5, 6}))  # frozenset({3, 4, 5})

# Нельзя изменять frozenset
# неизменяемое.add(6)  # AttributeError: 'frozenset' object has no attribute 'add'

# Можно использовать как ключ в словаре
словарь = {неизменяемое: "значение"}
print(словарь[неизменяемое])  # "значение"

Множественные включения (Set Comprehensions)

Аналогично списковым и словарным включениям, Python поддерживает множественные включения:

# Создание множества квадратов чисел
квадраты = {x**2 for x in range(10)}  # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

# С условием
четные_квадраты = {x**2 for x in range(10) if x % 2 == 0}  # {0, 4, 16, 36, 64}

# Преобразование строк в верхний регистр
слова = ["python", "java", "c++", "javascript", "python"]
верхний_регистр = {слово.upper() for слово in слова}  # {'PYTHON', 'JAVA', 'C++', 'JAVASCRIPT'}
# Обратите внимание, что дубликат "python" автоматически удален

Когда использовать множества

  • Для удаления дубликатов из коллекции
  • Когда нужна быстрая проверка принадлежности элемента
  • Для выполнения математических операций над множествами
  • Когда порядок элементов не важен
  • Когда нужно хранить только уникальные значения

Практические примеры

Пример 1: Удаление дубликатов

# Список с дубликатами
числа = [1, 2, 2, 3, 4, 4, 4, 5, 5]

# Удаление дубликатов с сохранением порядка
уникальные = list(dict.fromkeys(числа))  # [1, 2, 3, 4, 5]

# Альтернативный способ (не сохраняет порядок)
уникальные_множество = list(set(числа))  # [1, 2, 3, 4, 5], но порядок может быть другим

# Подсчет уникальных элементов
количество_уникальных = len(set(числа))  # 5

Пример 2: Поиск общих элементов

# Два списка с некоторыми общими элементами
список1 = ["яблоко", "груша", "банан", "апельсин", "киви"]
список2 = ["виноград", "банан", "манго", "киви", "ананас"]

# Найти общие элементы
общие = set(список1) & set(список2)  # {"банан", "киви"}

# Найти элементы, которые есть только в первом списке
только_в_первом = set(список1) - set(список2)  # {"яблоко", "груша", "апельсин"}

# Найти элементы, которые есть только во втором списке
только_во_втором = set(список2) - set(список1)  # {"виноград", "манго", "ананас"}

# Найти все уникальные элементы из обоих списков
все_уникальные = set(список1) | set(список2)
# {"яблоко", "груша", "банан", "апельсин", "киви", "виноград", "манго", "ананас"}

Пример 3: Проверка анаграмм

def проверить_анаграмму(строка1, строка2):
    """
    Проверяет, являются ли две строки анаграммами.
    Анаграмма - это слово, образованное перестановкой букв другого слова.
    """
    # Удаляем пробелы и приводим к нижнему регистру
    строка1 = строка1.replace(" ", "").lower()
    строка2 = строка2.replace(" ", "").lower()
    
    # Проверяем, совпадают ли множества символов и их количество
    return sorted(строка1) == sorted(строка2)

# Примеры
print(проверить_анаграмму("listen", "silent"))  # True
print(проверить_анаграмму("triangle", "integral"))  # True
print(проверить_анаграмму("hello", "world"))  # False

Пример 4: Анализ текста

def анализ_текста(текст):
    """Анализирует текст и возвращает статистику по словам."""
    # Очистка и разбиение текста на слова
    import re
    слова = re.findall(r'\b\w+\b', текст.lower())
    
    # Общее количество слов
    всего_слов = len(слова)
    
    # Уникальные слова
    уникальные_слова = set(слова)
    количество_уникальных = len(уникальные_слова)
    
    # Слова длиннее 5 символов
    длинные_слова = {слово for слово in уникальные_слова if len(слово) > 5}
    
    return {
        "всего_слов": всего_слов,
        "уникальных_слов": количество_уникальных,
        "процент_уникальных": round(количество_уникальных / всего_слов * 100, 2),
        "длинных_слов": len(длинные_слова),
        "примеры_длинных_слов": list(длинные_слова)[:3]  # Первые 3 длинных слова
    }

текст = """
Python - высокоуровневый язык программирования общего назначения. 
Стандартная библиотека Python включает большой набор полезных инструментов.
Python поддерживает несколько парадигм программирования.
"""

результат = анализ_текста(текст)
for ключ, значение in результат.items():
    print(f"{ключ}: {значение}")

Сравнение производительности

Множества обеспечивают очень быструю операцию проверки принадлежности элемента благодаря хеш-таблицам:

  • Список: x in my_list — O(n) (линейное время)
  • Множество: x in my_set — O(1) (константное время)

Для больших коллекций разница в производительности может быть значительной:

import time

# Создаем большой список и множество
большой_список = list(range(1000000))
большое_множество = set(большой_список)
искомый = 999999

# Измеряем время поиска в списке
начало = time.time()
результат_список = искомый in большой_список
конец = time.time()
время_список = конец - начало

# Измеряем время поиска в множестве
начало = time.time()
результат_множество = искомый in большое_множество
конец = time.time()
время_множество = конец - начало

print(f"Время поиска в списке: {время_список:.6f} секунд")
print(f"Время поиска в множестве: {время_множество:.6f} секунд")
print(f"Множество быстрее в {время_список / время_множество:.0f} раз")

4.4 Генераторы

Генераторы — это особый тип итераторов, который позволяет создавать последовательности значений "на лету", не храня их все в памяти одновременно. Это делает генераторы чрезвычайно эффективными для работы с большими наборами данных или бесконечными последовательностями.

Ключевые особенности генераторов:

  • Ленивые вычисления — значения создаются только при запросе
  • Экономия памяти — в памяти хранится только текущее значение
  • Одноразовый обход — генератор можно обойти только один раз
  • Возможность работы с бесконечными последовательностями
  • Поддержка протокола итератора (можно использовать в циклах for)

Создание генераторов с помощью функций

Функция-генератор использует ключевое слово yield вместо return для возврата значений:

def счетчик(максимум):
    """Простой генератор, возвращающий числа от 0 до максимум-1."""
    счет = 0
    while счет < максимум:
        yield счет
        счет += 1

# Использование генератора
for число in счетчик(5):
    print(число)  # Выведет числа 0, 1, 2, 3, 4

# Генератор можно преобразовать в список
список = list(счетчик(5))  # [0, 1, 2, 3, 4]

Как работают генераторы

Когда функция-генератор вызывается, она не выполняет свой код сразу. Вместо этого она возвращает объект-генератор, который можно итерировать. При каждом запросе нового значения (например, в цикле for) выполнение функции продолжается до следующего оператора yield.

def демонстрация():
    print("Первый шаг")
    yield 1
    print("Второй шаг")
    yield 2
    print("Третий шаг")
    yield 3
    print("Завершено")

# Создаем генератор
ген = демонстрация()

# Получаем значения по одному
print(next(ген))  # Выведет: Первый шаг, затем 1
print(next(ген))  # Выведет: Второй шаг, затем 2
print(next(ген))  # Выведет: Третий шаг, затем 3
# print(next(ген))  # Вызовет StopIteration, так как генератор исчерпан

Генераторные выражения

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

# Списковое включение (создает весь список сразу)
квадраты_список = [x**2 for x in range(10)]  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Генераторное выражение (создает значения по запросу)
квадраты_генератор = (x**2 for x in range(10))  # 

# Использование генераторного выражения
for квадрат in квадраты_генератор:
    print(квадрат)

# С условием
четные_квадраты = (x**2 for x in range(10) if x % 2 == 0)

# Вложенные циклы
координаты = ((x, y) for x in range(3) for y in range(2))

Преимущества генераторов

1. Экономия памяти

import sys

# Сравнение размера списка и генератора
список = [i for i in range(1000000)]
генератор = (i for i in range(1000000))

размер_списка = sys.getsizeof(список)
размер_генератора = sys.getsizeof(генератор)

print(f"Размер списка: {размер_списка:,} байт")
print(f"Размер генератора: {размер_генератора:,} байт")
print(f"Список больше генератора в {размер_списка / размер_генератора:.0f} раз")

2. Работа с большими файлами

def читать_большой_файл(имя_файла):
    """Читает файл построчно, не загружая его целиком в память."""
    with open(имя_файла, 'r', encoding='utf-8') as файл:
        for строка in файл:
            yield строка.strip()

# Использование
# for строка in читать_большой_файл('очень_большой_файл.txt'):
#     обработать(строка)

3. Создание бесконечных последовательностей

def бесконечные_числа():
    """Генерирует бесконечную последовательность чисел."""
    n = 0
    while True:
        yield n
        n += 1

# Использование с ограничением
счетчик = бесконечные_числа()
for _ in range(5):
    print(next(счетчик))  # Выведет 0, 1, 2, 3, 4

Генераторы и контекстные менеджеры

Генераторы можно использовать для создания собственных контекстных менеджеров с помощью декоратора contextlib.contextmanager:

from contextlib import contextmanager

@contextmanager
def открыть_файл(имя_файла, режим='r'):
    """Контекстный менеджер для работы с файлами."""
    try:
        файл = open(имя_файла, режим)
        yield файл
    finally:
        файл.close()

# Использование
# with открыть_файл('пример.txt') as f:
#     содержимое = f.read()

Расширенные возможности генераторов

1. Передача значений в генератор

Генераторы могут не только возвращать значения, но и принимать их с помощью метода send():

def эхо_генератор():
    """Генератор, который возвращает отправленные ему значения."""
    значение = yield "Готов к приему"  # Начальное значение
    while True:
        значение = yield f"Получено: {значение}"

# Использование
эхо = эхо_генератор()
print(next(эхо))  # "Готов к приему" (инициализация генератора)
print(эхо.send("Привет"))  # "Получено: Привет"
print(эхо.send(42))  # "Получено: 42"

2. Завершение генератора

Генераторы можно явно завершить с помощью методов close() или throw():

def генератор_с_очисткой():
    """Генератор с кодом очистки."""
    try:
        yield 1
        yield 2
        yield 3
    finally:
        print("Генератор завершен, ресурсы освобождены")

# Использование
ген = генератор_с_очисткой()
print(next(ген))  # 1
print(next(ген))  # 2
ген.close()  # Выведет: "Генератор завершен, ресурсы освобождены"

3. Делегирование генераторов (yield from)

С Python 3.3+ можно использовать yield from для делегирования части работы другому генератору:

def подгенератор():
    """Генерирует числа от 1 до 3."""
    yield 1
    yield 2
    yield 3

def основной_генератор():
    """Использует подгенератор и добавляет свои значения."""
    yield "Начало"
    yield from подгенератор()  # Делегирует работу подгенератору
    yield "Конец"

# Использование
for значение in основной_генератор():
    print(значение)
# Выведет: "Начало", 1, 2, 3, "Конец"

Когда использовать генераторы

  • Для работы с большими наборами данных, которые не помещаются в память
  • Для последовательностей, которые требуют сложных вычислений
  • Для бесконечных последовательностей
  • Когда нужно обработать данные построчно или поэлементно
  • Для создания пользовательских итераторов

Практические примеры

Пример 1: Генерация последовательности Фибоначчи

def фибоначчи(n):
    """Генератор для первых n чисел Фибоначчи."""
    a, b = 0, 1
    счетчик = 0
    while счетчик < n:
        yield a
        a, b = b, a + b
        счетчик += 1

# Вывод первых 10 чисел Фибоначчи
for число in фибоначчи(10):
    print(число)  # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

# Бесконечная последовательность Фибоначчи
def бесконечный_фибоначчи():
    """Генератор для бесконечной последовательности Фибоначчи."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Использование с ограничением
генератор = бесконечный_фибоначчи()
for _ in range(10):
    print(next(генератор))

Пример 2: Обработка данных построчно

def обработать_лог(имя_файла):
    """Обрабатывает лог-файл построчно, извлекая IP-адреса."""
    import re
    pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'  # Простой паттерн для IP-адресов
    
    with open(имя_файла, 'r') as файл:
        for строка in файл:
            ip_адреса = re.findall(pattern, строка)
            if ip_адреса:
                yield ip_адреса[0], строка.strip()

# Использование
# for ip, строка in обработать_лог('access.log'):
#     print(f"IP: {ip}, Строка: {строка[:50]}...")

Пример 3: Пагинация данных

def пагинация(данные, размер_страницы=10):
    """Разбивает большой список на страницы заданного размера."""
    for i in range(0, len(данные), размер_страницы):
        yield данные[i:i + размер_страницы]

# Пример использования
элементы = list(range(1, 35))  # Список из 34 элементов

for i, страница in enumerate(пагинация(элементы, 10), 1):
    print(f"Страница {i}: {страница}")

Пример 4: Генерация комбинаций

def комбинации(список, длина):
    """Генерирует все возможные комбинации элементов списка заданной длины."""
    if длина == 0:
        yield []
        return
        
    for i in range(len(список)):
        текущий = список[i]
        оставшиеся = список[i + 1:]
        
        for комб in комбинации(оставшиеся, длина - 1):
            yield [текущий] + комб

# Использование
цвета = ["красный", "зеленый", "синий", "желтый"]

print("Комбинации из 2 цветов:")
for комб in комбинации(цвета, 2):
    print(комб)

Пример 5: Конвейерная обработка данных

def читать_числа(файл):
    """Читает числа из файла."""
    for строка in файл:
        for число in строка.split():
            yield int(число)

def только_четные(числа):
    """Фильтрует только четные числа."""
    for число in числа:
        if число % 2 == 0:
            yield число

def квадраты(числа):
    """Возводит числа в квадрат."""
    for число in числа:
        yield число ** 2

# Использование конвейера генераторов
# with open('числа.txt', 'r') as файл:
#     конвейер = квадраты(только_четные(читать_числа(файл)))
#     for результат in конвейер:
#         print(результат)

Генераторы vs Списки

Аспект Списки Генераторы
Память Хранят все элементы сразу Хранят только текущий элемент
Производительность Быстрый доступ к элементам Меньшее потребление памяти
Повторное использование Можно обходить многократно Только однократный обход
Синтаксис [x for x in range(10)] (x for x in range(10))
Бесконечные последовательности Невозможно Возможно

Настройки

Цветовая схема

Тема