Модуль 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)) |
Бесконечные последовательности | Невозможно | Возможно |
Поздравляем!
Вы прошли модуль "Структуры данных" в Python. Теперь вы знаете, как эффективно организовывать и обрабатывать данные с помощью списков, кортежей, словарей, множеств и генераторов.