ТЕОРИЯ И ПРАКТИКА

  • Ввод и вывод данных
    • Задачи
  • Условия
    • Задачи
  • Цикл for
    • Задачи
  • Строки
    • Задачи
  • Цикл while
    • Задачи
  • Списки
    • Задачи
  • Двумерные массивы
    • Задачи
  • Словари
    • Задачи
  • Множества
    • Задачи
  • Функции и рекурсия
    • Задачи

Занятие 8. Словари в питоне

Введение в словари

Словарь в Python: структура данных хеш-таблица

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

Внутри словарь работает так:

  1. Вы предоставляете ключ (например, слово "яблоко").
  2. Python вычисляет хеш от этого ключа — уникальное числовое представление.
  3. Это число используется как индекс для поиска ячейки в памяти, где хранится связанное с ключом значение (например, "красный фрукт").

Благодаря этому механизму поиск, добавление и удаление элементов происходят практически мгновенно, независимо от размера словаря.

Основные свойства словарей

Коллекция пар "ключ — значение"

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

# Пример: словарь с информацией о пользователе
user = {
    "name": "Alex",
    "age": 30,
    "is_admin": True
}
print(user)
                        

Ключи уникальны

В одном словаре не может быть двух одинаковых ключей. Если вы попытаетесь добавить пару с уже существующим ключом, новое значение просто перезапишет старое.

config = {"host": "localhost", "port": 8080}
print(f"Изначальный порт: {config['port']}")

# Попытка добавить пару с существующим ключом "port"
config["port"] = 9000
print(f"Новый порт: {config['port']}")
print(f"Весь словарь: {config}")
                        

Значения могут повторяться

В отличие от ключей, значения могут быть какими угодно и повторяться сколько угодно раз.

# Разные студенты могут иметь одинаковые оценки
grades = {
    "Иван": 5,
    "Мария": 4,
    "Петр": 5
}
print(grades)
                        

Отличие словаря от списка и множества

  • Словарь vs Список (list): В списках элементы упорядочены и доступны по числовому индексу (0, 1, 2...). В словарях элементы доступны по ключу, который может быть строкой, числом или другим неизменяемым типом. Поиск по ключу в словаре (O(1)) намного быстрее, чем поиск элемента в списке (O(n)).
  • Словарь vs Множество (set): Множества хранят только уникальные значения (без ключей). Они хороши для проверки на наличие элемента и математических операций (объединение, пересечение), но не для хранения связанных данных.

Практические применения словарей

Словари используются повсеместно:

  • Данные в формате JSON: Веб-API почти всегда возвращают данные в виде, который легко преобразуется в словари Python.
  • Настройки конфигурации: Хранение параметров программы.
  • Представление объектов: Например, пользователя, продукта, заказа.
  • Кэширование: Хранение результатов "дорогих" вычислений.
  • Подсчёт частоты: Быстрый подсчёт уникальных элементов в коллекции.

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

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

Через {}

Самый распространённый и предпочтительный способ — использование фигурных скобок.

empty_dict_1 = {}
print(f"Тип: {type(empty_dict_1)}, Содержимое: {empty_dict_1}")
                        

Через функцию dict()

Можно также использовать встроенную функцию dict().

empty_dict_2 = dict()
print(f"Тип: {type(empty_dict_2)}, Содержимое: {empty_dict_2}")
                        

Словарь с заданными значениями

Явное указание пар ключ-значение

Это основной способ создания словаря с начальными данными.

person = {
    "first_name": "John",
    "last_name": "Doe",
    "age": 25
}
print(person)
                        

Через функцию dict() с именованными аргументами

Этот способ удобен, когда ключи являются простыми строками без пробелов и спецсимволов.

person_alt = dict(first_name="Jane", last_name="Doe", age=28)
print(person_alt)
                        

Создание словаря из списков с помощью zip()

Если у вас есть два списка (один для ключей, другой для значений), их можно "сшить" в словарь с помощью zip() и dict().

keys = ["brand", "model", "year"]
values = ["Toyota", "Camry", 2022]
car = dict(zip(keys, values))
print(car)
                        

Генераторы словарей (dict comprehension)

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

# Создать словарь, где ключ - число, а значение - его квадрат
squares = {x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
                        

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

Получение значения по ключу через []

Основной способ доступа к значению — указать его ключ в квадратных скобках. Внимание: если ключа не существует, это вызовет ошибку KeyError.

user = {"name": "Alex", "age": 30}
print(user["name"]) # Выведет "Alex"

# Следующая строка вызовет ошибку KeyError, так как ключа 'city' нет
# print(user["city"])
                        

Получение значения через метод get()

Метод get() — безопасный способ доступа. Он не вызывает ошибку, если ключа нет.

Без значения по умолчанию

Если ключ не найден, get() по умолчанию возвращает None.

user = {"name": "Alex", "age": 30}
city = user.get("city")
print(f"Имя: {user.get('name')}")
print(f"Город: {city}") # Выведет None, ошибки не будет
                        

С указанием значения по умолчанию

Вторым аргументом в get() можно передать значение, которое вернётся, если ключ не будет найден.

user = {"name": "Alex", "age": 30}
# Если ключ 'city' не найден, вернуть строку 'Не указан'
city = user.get("city", "Не указан")
print(f"Город: {city}") # Выведет 'Не указан'
                        

Обработка ошибок доступа к несуществующему ключу

Если вы используете [], но не уверены в наличии ключа, используйте конструкцию try...except.

user = {"name": "Alex", "age": 30}
try:
    city = user["city"]
    print(f"Город: {city}")
except KeyError:
    print("Ключ 'city' не найден в словаре.")
                        

Изменение и добавление элементов

Присваивание значения по ключу

Синтаксис для изменения существующего элемента и добавления нового абсолютно одинаков.

Изменение существующего

Если ключ уже есть в словаре, его значение будет обновлено.

user = {"name": "Alex", "age": 30}
print(f"Старый возраст: {user['age']}")
user["age"] = 31 # Обновляем значение по ключу 'age'
print(f"Новый возраст: {user['age']}")
                        

Добавление нового

Если ключа в словаре нет, будет создана новая пара "ключ-значение".

user = {"name": "Alex", "age": 30}
print(f"Словарь до добавления: {user}")
user["email"] = "alex@example.com" # Добавляем новую пару
print(f"Словарь после добавления: {user}")
                        

Удаление элементов из словаря

Метод pop()

Удаляет пару по указанному ключу и возвращает удалённое значение. Если ключ не найден, вызывает KeyError.

user = {"name": "Alex", "age": 30, "email": "alex@example.com"}
removed_age = user.pop("age")
print(f"Удаленное значение: {removed_age}")
print(f"Словарь после удаления: {user}")

# Можно указать значение по умолчанию, чтобы избежать ошибки
removed_city = user.pop("city", "Город не был указан")
print(f"Попытка удалить несуществующий ключ: {removed_city}")
                        

Метод popitem()

Удаляет и возвращает последнюю добавленную пару (ключ, значение) в виде кортежа. Работает по принципу LIFO (Last-In, First-Out). Если словарь пуст, вызывает KeyError.

user = {"name": "Alex", "email": "alex@example.com"}
last_item = user.popitem()
print(f"Удаленная пара: {last_item}")
print(f"Словарь после popitem: {user}")
                        

Оператор del

Удаляет пару по ключу. Ничего не возвращает. Если ключ не найден, вызывает KeyError.

user = {"name": "Alex", "age": 30}
del user["age"]
print(f"Словарь после del: {user}")
                        

Метод clear()

Удаляет все элементы из словаря, делая его пустым.

user = {"name": "Alex", "age": 30}
user.clear()
print(f"Словарь после clear: {user}")
                        

Методы словаря Python: доступ и операции

Метод get()

Как мы уже видели, безопасно получает значение по ключу, возвращая None или значение по умолчанию, если ключ отсутствует.

Метод keys()

Возвращает специальный объект-представление (dict_keys), содержащий все ключи словаря. Его можно перебирать в цикле или преобразовать в список.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_keys = car.keys()
print(f"Объект ключей: {all_keys}")
print(f"Ключи в виде списка: {list(all_keys)}")
                        

Получение значений из словаря: values()

Возвращает объект-представление (dict_values) со всеми значениями словаря.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_values = car.values()
print(f"Объект значений: {all_values}")
print(f"Значения в виде списка: {list(all_values)}")
                        

Методы, возвращаемые items() dictionary

Возвращает объект-представление (dict_items) с парами (ключ, значение) в виде кортежей. Это самый удобный способ для перебора словаря целиком.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_items = car.items()
print(f"Объект пар: {all_items}")
print(f"Пары в виде списка: {list(all_items)}")
                        

Метод update()

Обновляет словарь, добавляя пары ключ-значение из другого словаря или итерируемого объекта. Если ключи совпадают, значения перезаписываются.

user_info = {"name": "Alice", "age": 25}
user_contacts = {"email": "alice@email.com", "age": 26} # возраст будет обновлен

user_info.update(user_contacts)
print(user_info)
                        

Метод fromkeys()

Создаёт новый словарь из переданной последовательности ключей. Всем ключам присваивается одно и то же значение (по умолчанию None).

keys = ['a', 'b', 'c']
# Создать словарь с ключами из списка и значением 0 для каждого
new_dict = dict.fromkeys(keys, 0)
print(new_dict) # {'a': 0, 'b': 0, 'c': 0}

# Если значение не указать, будет None
default_dict = dict.fromkeys(keys)
print(default_dict) # {'a': None, 'b': None, 'c': None}
                        

Перебор словаря

Перебор только ключей

Это поведение по умолчанию при итерации по словарю.

person = {"name": "Bob", "age": 42, "city": "New York"}
for key in person:
    print(key)
# или явно
for key in person.keys():
    print(key)
                        

Перебор только значений

Для этого используется метод values().

person = {"name": "Bob", "age": 42, "city": "New York"}
for value in person.values():
    print(value)
                        

Перебор пар ключ-значение через items()

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

person = {"name": "Bob", "age": 42, "city": "New York"}
for key, value in person.items():
    print(f"Ключ: {key}, Значение: {value}")
                        

Перебор с условием и фильтрацией

Циклы легко комбинируются с условными операторами if.

products = {"apple": 50, "banana": 20, "orange": 80, "milk": 120}
# Найти все продукты, цена которых больше 60
for product, price in products.items():
    if price > 60:
        print(f"{product} стоит {price}")
                        

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

Структура словаря в словаре

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

# Словарь пользователей, где каждый пользователь - это тоже словарь
users = {
    "user1": {
        "name": "Alice",
        "email": "alice@example.com"
    },
    "user2": {
        "name": "Bob",
        "email": "bob@example.com"
    }
}
print(users)
                        

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

Доступ осуществляется по цепочке ключей.

users = {
    "user1": { "name": "Alice", "email": "alice@example.com" },
    "user2": { "name": "Bob", "email": "bob@example.com" }
}
# Получить email пользователя user2
email_bob = users["user2"]["email"]
print(email_bob) # bob@example.com
                        

Изменение и добавление элементов во вложенные словари

Принцип тот же: добираемся до нужного места по цепочке ключей и присваиваем новое значение.

users = {
    "user1": { "name": "Alice", "email": "alice@example.com" }
}

# Изменить имя пользователя user1
users["user1"]["name"] = "Alicia"

# Добавить возраст пользователю user1
users["user1"]["age"] = 30

print(users)
                        

Перебор вложенных словарей

Используются вложенные циклы.

users = {
    "user1": { "name": "Alice", "email": "alice@example.com" },
    "user2": { "name": "Bob", "email": "bob@example.com" }
}

# Перебрать всех пользователей и их данные
for user_id, user_data in users.items():
    print(f"ID пользователя: {user_id}")
    for key, value in user_data.items():
        print(f"  {key}: {value}")
                        

Практические задачи

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

Классическая задача, идеально решаемая с помощью словаря.

text = "hello world"
frequency = {}

for char in text:
    # Используем get() с значением по умолчанию 0
    frequency[char] = frequency.get(char, 0) + 1

print(frequency) # {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}
                        

Группировка данных по общему признаку

Например, сгруппируем список людей по городам.

people = [
    {"name": "Alice", "city": "New York"},
    {"name": "Bob", "city": "London"},
    {"name": "Charlie", "city": "New York"},
    {"name": "Diana", "city": "London"}
]

grouped_by_city = {}
for person in people:
    city = person["city"]
    # Если города еще нет в словаре, создаем для него пустой список
    if city not in grouped_by_city:
        grouped_by_city[city] = []
    # Добавляем человека в список его города
    grouped_by_city[city].append(person["name"])

print(grouped_by_city) # {'New York': ['Alice', 'Charlie'], 'London': ['Bob', 'Diana']}
                        

Объединение двух словарей

Через update()

Метод update() изменяет исходный словарь.

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

dict1.update(dict2) # dict1 будет изменен
print(dict1) # {'a': 1, 'b': 3, 'c': 4}
                        

Через оператор **

Оператор распаковки ** (Python 3.5+) позволяет создать новый объединенный словарь, не изменяя исходные.

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

# При совпадении ключей будет взято значение из последнего словаря (dict2)
merged_dict = {**dict1, **dict2}
print(merged_dict) # {'a': 1, 'b': 3, 'c': 4}
print(dict1) # {'a': 1, 'b': 2} - остался без изменений
                        

Инвертирование словаря

Обратная замена ключей и значений

Простой случай, когда все значения уникальны.

original = {'a': 1, 'b': 2, 'c': 3}
inverted = {value: key for key, value in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
                        

Обработка одинаковых значений

Если значения не уникальны, простая инверсия приведет к потере данных. Правильное решение — группировать ключи в список.

original = {'a': 1, 'b': 2, 'c': 1} # Значение '1' повторяется
inverted_grouped = {}

for key, value in original.items():
    if value not in inverted_grouped:
        inverted_grouped[value] = []
    inverted_grouped[value].append(key)

print(inverted_grouped) # {1: ['a', 'c'], 2: ['b']}
                        

Генерация словарей (dict comprehension)

Простые генераторы: {x: f(x) for x in ...}

Лаконичный синтаксис для создания словарей.

# Словарь {1: 1, 2: 4, 3: 9, ...}
squares = {x: x * x for x in range(1, 6)}
print(squares)
                        

Генераторы с условием

Можно добавить if для фильтрации элементов.

# Создать словарь только для нечетных чисел
odd_squares = {x: x * x for x in range(1, 10) if x % 2 != 0}
print(odd_squares) # {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
                        

Генераторы на основе zip() или enumerate()

Можно использовать другие функции для получения пар.

# zip
keys = ['name', 'age']
values = ['Tom', 33]
person = {k: v for k, v in zip(keys, values)}
print(person) # {'name': 'Tom', 'age': 33}

# enumerate
items = ['apple', 'banana', 'cherry']
indexed_items = {index: value for index, value in enumerate(items)}
print(indexed_items) # {0: 'apple', 1: 'banana', 2: 'cherry'}
                        

Полезные приёмы и трюки

Проверка существования ключа через in

Самый быстрый и "питоничный" (Pythonic) способ проверить, есть ли ключ в словаре.

user = {"name": "Frank", "age": 50}
if "age" in user:
    print("Ключ 'age' существует.")
if "city" not in user:
    print("Ключ 'city' отсутствует.")
                        

Использование setdefault()

Метод setdefault() похож на get(), но с одним важным отличием: если ключ отсутствует, он не только возвращает значение по умолчанию, но и добавляет в словарь новую пару с этим ключом и значением. Это очень удобно для инициализации коллекций.

# Классический пример группировки с setdefault()
data = [("fruits", "apple"), ("vegetables", "carrot"), ("fruits", "banana")]
grouped = {}
for category, item in data:
    # Если 'category' нет, создастся ключ со значением [] и вернется [].
    # Если есть, просто вернется существующий список.
    # В любом случае, мы можем сразу делать .append()
    grouped.setdefault(category, []).append(item)

print(grouped) # {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}
                        

Подсчёт и накопление значений

Использование словарей как счётчиков

Эти два пункта тесно связаны. Словарь — идеальный инструмент для подсчёта.

from collections import Counter

# Простой способ (уже показывали)
words = ["apple", "orange", "apple", "banana", "orange", "apple"]
word_counts = {}
for word in words:
    word_counts[word] = word_counts.get(word, 0) + 1
print(word_counts)

# Продвинутый способ с использованием специального класса Counter
word_counts_pro = Counter(words)
print(word_counts_pro) # Counter({'apple': 3, 'orange': 2, 'banana': 1})
# Counter - это подкласс dict, у него есть все те же методы и еще свои.
print(word_counts_pro.most_common(2)) # [('apple', 3), ('orange', 2)]
                        

Использование словарей как хранилищ (например, кэш)

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

import time

# Словарь для кэша
fib_cache = {}

def fibonacci(n):
    # Если значение уже есть в кэше, вернуть его
    if n in fib_cache:
        return fib_cache[n]
    
    # Иначе, вычислить
    if n <= 1:
        result = n
    else:
        result = fibonacci(n - 1) + fibonacci(n - 2)
    
    # Сохранить результат в кэш перед возвратом
    fib_cache[n] = result
    return result

start_time = time.time()
print(fibonacci(35))
print(f"Время выполнения: {time.time() - start_time:.4f} секунд")

# Второй вызов будет почти мгновенным, так как все значения уже в кэше
start_time = time.time()
print(fibonacci(35))
print(f"Время второго вызова: {time.time() - start_time:.4f} секунд")
                        

Преобразование словаря

Ключи/значения/пары в список

my_dict = {'a': 1, 'b': 2, 'c': 3}
key_list = list(my_dict.keys())   # или просто list(my_dict)
value_list = list(my_dict.values())
item_list = list(my_dict.items()) # список кортежей

print(f"Ключи: {key_list}")
print(f"Значения: {value_list}")
print(f"Пары: {item_list}")
                        

В JSON (сериализация)

JSON (JavaScript Object Notation) — текстовый формат обмена данными. Структура словарей Python идеально ему соответствует. Процесс преобразования в строку JSON называется сериализацией.

import json

user = {
    "name": "Иван Петров",
    "age": 30,
    "is_active": True,
    "courses": ["Python", "Git"],
    "passport": None
}

# Преобразовать словарь в строку JSON
# ensure_ascii=False для корректного отображения кириллицы
# indent=4 для красивого форматирования с отступами
json_string = json.dumps(user, ensure_ascii=False, indent=4)
print(json_string)

# Обратное преобразование (десериализация)
back_to_dict = json.loads(json_string)
print(back_to_dict)
                        

Из других структур (например, list of tuples)

Словарь можно создать из любой последовательности, элементы которой сами являются последовательностями из двух элементов (ключ, значение).

list_of_tuples = [('a', 1), ('b', 2), ('c', 3)]
my_dict = dict(list_of_tuples)
print(my_dict)
                        

Расширенные структуры

Словари с вложенными списками

Очень распространённая структура для хранения связанных коллекций.

user_permissions = {
    "admin": ["create_user", "delete_user", "edit_content"],
    "editor": ["edit_content"],
    "viewer": ["view_content"]
}

# Проверить, может ли админ удалять пользователей
can_delete = "delete_user" in user_permissions["admin"]
print(f"Админ может удалять пользователей: {can_delete}")
                        

Словари с множествами

Используйте множество (set) в качестве значения, если вам важна уникальность элементов и быстрая проверка на вхождение.

user_tags = {
    "post1": {"python", "web", "django"},
    "post2": {"python", "data-science"},
    "post3": {"web", "css"}
}

# Найти все посты с тегом 'python'
python_posts = [post_id for post_id, tags in user_tags.items() if "python" in tags]
print(f"Посты с тегом 'python': {python_posts}")
                        

Словари с функциями как значениями

Функции в Python являются объектами первого класса, их можно хранить в словарях. Это позволяет реализовать, например, паттерн "фабрика" или "диспетчер".

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

# Словарь, который сопоставляет строковые операторы с функциями
operations = {
    "+": add,
    "-": subtract
}

operator = "+"
x, y = 10, 5

# Выбрать нужную функцию из словаря и вызвать ее
result = operations[operator](x, y)
print(f"{x} {operator} {y} = {result}")
                        

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

Ошибка KeyError в Python

Возникает при попытке доступа по несуществующему ключу через квадратные скобки []. Как избежать:

  1. Использовать dict.get().
  2. Проверять наличие ключа через if key in my_dict.
  3. Использовать try...except KeyError.

Использование изменяемых объектов как ключей

Ключи словаря должны быть неизменяемыми (immutable) и хешируемыми (hashable). Это строки, числа, кортежи (tuple). Списки (list), множества (set) и другие словари (dict) не могут быть ключами, так как они изменяемы.

valid_dict = {}
valid_dict["string_key"] = 1
valid_dict[123] = 2
valid_dict[(1, 2)] = 3 # Кортеж - неизменяемый, можно использовать как ключ

print(f"Рабочий словарь: {valid_dict}")

# Следующий код вызовет ошибку TypeError: unhashable type: 'list'
# invalid_dict = {}
# invalid_dict[[1, 2]] = "не сработает"
                        

Поверхностное vs глубокое копирование словарей

Это важная концепция при работе с вложенными структурами.

  • Поверхностное копирование (.copy()): Создаётся новый словарь, но в него копируются ссылки на вложенные объекты. Изменение вложенного объекта в копии затронет и оригинал.
  • Глубокое копирование (copy.deepcopy()): Создаётся полная, независимая копия, включая все вложенные объекты.
import copy

original = {
    "a": 1,
    "b": [10, 20, 30] # Вложенный изменяемый список
}

# Поверхностная копия
shallow = original.copy()
shallow["b"].append(40) # Изменяем список в копии
print(f"Оригинал после изменения поверхностной копии: {original}") # Оригинал тоже изменился!

# Глубокая копия
original = {"a": 1, "b": [10, 20, 30]} # Восстановим
deep = copy.deepcopy(original)
deep["b"].append(40) # Изменяем список в копии
print(f"Оригинал после изменения глубокой копии: {original}") # Оригинал не изменился
                        

Производительность и оптимизация

Быстродействие операций со словарями

Благодаря хеш-таблицам, основные операции (добавление, получение, удаление элемента по ключу) в среднем выполняются за константное время, O(1). Это означает, что время выполнения не зависит от количества элементов в словаре.

Сравнение с другими структурами данных

  • Словарь (поиск по ключу): O(1)
  • Список (поиск по значению): O(n) - нужно перебрать все элементы.
  • Множество (проверка наличия элемента): O(1)

Вывод: если вам нужен быстрый поиск по уникальному идентификатору, словарь — лучший выбор.

Кэширование результатов с помощью словарей

Как было показано в ранее, словари — это идеальный инструмент для реализации кэширования (мемоизации), что является одной из ключевых техник оптимизации производительности.