2.6 Множества и словари#
В предыдущей теме мы рассмотрели последовательности - упорядоченные коллекции, к элементам которых можно обратиться по индексу. Но в Python есть и другие типы коллекций, которые не являются последовательностями. В этой теме мы познакомимся с двумя из них: множествами и словарями. Оба этих типа неупорядочены и основаны на одном и том же механизме - хешировании.
Множества#
Множества не относятся к последовательностям - это неупорядоченная коллекция уникальных элементов. У множеств нет индексов, и обратиться к элементу по номеру нельзя. Основное применение множеств - поиск уникальных значений и операции над наборами данных (объединение, пересечение, разность).
Создание множеств#
Множество создаётся путём перечисления в фигурных скобках через запятую элементов, которые в него должны войти (изменяемые типы данных в множество добавить нельзя):
number_set = {1, 2, 3, 4}
print(number_set)
{1, 2, 3, 4}
number_set = {1, 1, 1, 1} # При создании множества элементы могут повторяться
print(number_set) # После создания остаются только уникальные
{1}
Также множество можно создать через вызов set(), передав ему любую последовательность:
# Из списка
set_from_list = set([1, 2, 3, 4])
print(set_from_list)
# Из строки
set_from_string = set("hello")
print(set_from_string) # строка разбивается на уникальные символы
# Из диапазона
set_from_range = set(range(5))
print(set_from_range)
{1, 2, 3, 4}
{'o', 'h', 'l', 'e'}
{0, 1, 2, 3, 4}
Пустое множество создаётся только через set() - пустые фигурные скобки {} создают пустой словарь:
empty_set = set()
print(empty_set, type(empty_set))
not_a_set = {} # это словарь, а не множество!
print(not_a_set, type(not_a_set))
set() <class 'set'>
{} <class 'dict'>
Основное применение множеств будет в поиске уникальных значений для списков значений:
number_sequence = [1, 2, 3, 4, 4, 3, 2, 1] # список с повторяющимися значениями
number_set = set(number_sequence) # преобразуем в множество
print(number_set) # остались только уникальные
{1, 2, 3, 4}
Добавление и удаление элементов#
Множества - изменяемый тип данных, поэтому в них можно добавлять новые элементы и удалять существующие. Для добавления одного элемента используется метод add:
cities = {"Moscow", "Saint Petersburg"}
cities.add("Novosibirsk")
print(cities)
{'Saint Petersburg', 'Moscow', 'Novosibirsk'}
Если элемент уже существует, add просто ничего не добавит - дубликаты не создадутся:
cities.add("Moscow") # Moscow уже есть, ничего не изменится
print(cities)
{'Saint Petersburg', 'Moscow', 'Novosibirsk'}
Для удаления элемента есть два метода:
discard(element)- удаляет элемент, если он есть. Если элемента нет - ничего не делает (безопасный способ)remove(element)- удаляет элемент, если он есть. Если элемента нет - вызывает ошибку
cities.discard("Novosibirsk") # удалит, ошибки не будет
print(cities)
cities.discard("Kazan") # Kazan нет во множестве, но ошибки не будет
print(cities)
{'Saint Petersburg', 'Moscow'}
{'Saint Petersburg', 'Moscow'}
Практическое применение#
Основное применение множеств - работа с уникальными значениями. Если у нас есть список с дубликатами, самый быстрый способ получить уникальные элементы - привести его к множеству:
# Данные с дубликатами
raw_data = ["Moscow", "Moscow", "Saint Petersburg", "Moscow", "Kazan", "Saint Petersburg"]
# Один вызов set - и дубликаты убраны
unique_cities = set(raw_data)
print("Уникальные города:", unique_cities)
print("Количество:", len(unique_cities))
Уникальные города: {'Saint Petersburg', 'Kazan', 'Moscow'}
Количество: 3
Если данные приходят постепенно, множество можно наполнять по одному элементу через add() - дубликаты всё равно не добавятся.
Ещё одно важное применение множеств - быстрая проверка вхождения. Оператор in для множества работает быстрее, чем для списка. Список хранит элементы упорядоченно, и для проверки in ему нужно перебрать элементы один за другим. А множество находит нужный элемент сразу - без перебора:
allowed_types = {"Point", "Line", "Polygon"}
feature_type = "Point"
if feature_type in allowed_types:
print(f"{feature_type} - допустимый тип")
Point - допустимый тип
На небольших данных разница незаметна, но когда список допустимых значений большой, множество будет значительно быстрее.
Как работает хеширование
Множества и словари в Python построены на хеш-таблице - структуре данных, которая хранит элементы в специальных «ячейках» (buckets).
Когда элемент добавляется в множество:
Python вычисляет его хеш - числовой «отпечаток» через функцию
hash()По хешу определяется номер ячейки:
hash(элемент) % размер_таблицыЭлемент помещается в эту ячейку
При проверке in Python делает то же самое: вычисляет хеш → идёт в нужную ячейку → проверяет, есть ли там элемент. Поэтому поиск мгновенный - не нужно перебирать все элементы.
Из этого следует важное ограничение: элементы множества и ключи словаря должны быть неизменяемыми. Если бы объект мог измениться после добавления, его хеш бы изменился, и Python больше не смог бы найти этот элемент - он «потерялся» бы в таблице.
Подробнее про хеш-таблицы в Python: Хеширование в Python
Словари#
Последний тип данных, который мы отнесли к коллекциям, - это отображения. В Python отображения представлены таким типом данных, как словари - dict.
Словарь в Python - это неупорядоченная изменяемая коллекция, которая хранит пары ключ-значение. Неупорядоченность означает, что у словаря невозможно обратиться к значению по индексу. Также важно отметить, что ключи должны быть уникальными и неизменяемыми (например, строки, числа или кортежи), а значения могут быть любого типа данных.
Что может быть ключом
На самом деле ключи словаря должны быть хешируемыми объектами. Это то же самое требование, что и для элементов множества. Благодаря этому словарь, как и множество, быстро находит значение по ключу без перебора всех пар.
Создание словарей#
Для определения словаря, как и в случае с множествами, используется перечисление в фигурных скобках. Единственное отличие, что через запятую перечисляются не просто значения, а именно пары, разделенные двоеточием:
new_dict = {"Russia": "Moscow", "USA": "Washington"}
print(new_dict)
{'Russia': 'Moscow', 'USA': 'Washington'}
Часто в рамках задач нужно будет не использовать готовые словари, а создавать пустые и заполнять их в процессе. Для создания пустого словаря можно пользоваться двумя вариантами:
empty_dict = dict()
empty_dict_2 = {}
print(empty_dict)
print(empty_dict_2)
{}
{}
Получение данных#
В отличие от последовательностей, где элемент можно было получить по его порядковому номеру, словари неупорядочены, и у них нет обращения по индексам, вместо этого имеется обращение по ключу.
Для обращения по ключу есть несколько способов:
указать нужный ключ в квадратных скобках -
dict[key]использовать метод get -
dict.get(key, value_if_no_key)использовать метод setdefault -
dict.setdefault(key, create_value_if_no_key)
point = {"x": 10, "y": 12, "z": 5}
x = point["x"] # такое обращение по ключу, которого не существует, вызовет ошибку.
y = point.get("y") # обращение по ключу, которого не существует, вернет None. Безопасный способ
m = point.get("m", "ключ отсутствует") # обращение по ключу, которого не существует, вернет второй аргумент
print(x, y, m)
10 12 ключ отсутствует
m = point.setdefault("m", 100) # если ключа не существует, то он будет создан. После этого вернет значение по ключу
print(point, m)
{'x': 10, 'y': 12, 'z': 5, 'm': 100} 100
В реальных задачах, чтобы обезопасить себя от ошибок, в случаях, когда нужно получить конкретное значение по ключу, стоит использовать метод get. Также возникают ситуации, когда требуется получить список всех ключей или значений, для этого у словаря есть специальные методы:
keys()- метод, возвращающий список ключейvalues()- метод, возвращающий список значенийitems()- метод, возвращающий пары значений(key, value)
point = {"x": 10, "y": 12, "z": 12}
keys = point.keys() # на самом деле возвращает специальный тип данных dict_keys
print(keys)
print(list(keys)) # dict_keys можно преобразовать к обычному списку
dict_keys(['x', 'y', 'z'])
['x', 'y', 'z']
point = {"x": 10, "y": 12, "z": 12}
values = point.values() # на самом деле возвращает специальный тип данных dict_values
print(values)
print(list(values)) # dict_values можно преобразовать к обычному списку
dict_values([10, 12, 12])
[10, 12, 12]
point = {"x": 10, "y": 12, "z": 12}
items = point.items() # на самом деле возвращает специальный тип данных dict_items
print(items)
print(list(items)) # dict_items можно преобразовать к обычному списку
dict_items([('x', 10), ('y', 12), ('z', 12)])
[('x', 10), ('y', 12), ('z', 12)]
Изменение словарей#
Для обновления конкретного ключа можно использовать конструкцию, аналогичную тому, как изменяется значение по индексу в списке:
point = {"x": 10, "y": 12, "z": 5}
point["z"] = 100 # если ключ уже существовал, то обновит его значение
print(point)
{'x': 10, 'y': 12, 'z': 100}
Примечательно, что если при использовании такой инструкции указать несуществующий ключ, то ошибки не произойдет, а просто будет создан новый ключ:
point = {"x": 10, "y": 12, "z": 5}
point["t"] = 100 # если ключа нет в словаре, он будет создан
print(point)
{'x': 10, 'y': 12, 'z': 5, 't': 100}
Также один словарь можно обновлять с использованием другого словаря.
Так, для обновления словаря используется метод update, принимающий в качестве аргумента ещё один словарь. В результате команды a.update(b) - для совпадающих ключей значения будут обновлены, а ключи из b, отсутствующие в a, будут созданы с соответствующими значениями.
point = {"x": 10, "y": 12, "z": 12}
zm_point = {"z": 5, "m": 100}
point.update(zm_point) # ключ z - обновится, ключ m - добавится
print(point)
{'x': 10, 'y': 12, 'z': 5, 'm': 100}
Практическое применение#
Словари удобно использовать для структурирования данных, например, описания объектов с разными атрибутами. Так, можно описать город, храня его характеристики в одном словаре:
city = {
"name": "Moscow",
"population": 12_000_000,
"coordinates": (55.7558, 37.6173)
}
print(city)
{'name': 'Moscow', 'population': 12000000, 'coordinates': (55.7558, 37.6173)}
Если появятся новые данные - их легко добавить:
city["area"] = 2562 # площадь в км²
city["founded"] = 1147 # год основания
print(city)
{'name': 'Moscow', 'population': 12000000, 'coordinates': (55.7558, 37.6173), 'area': 2562, 'founded': 1147}
Также словари могут использоваться для группировки данных. Представьте, что у нас есть список географических объектов разного типа - города, реки, горы - и мы хотим разложить их по категориям. Словарь идеально подходит для этой задачи: тип объекта будет ключом, а список объектов - значением:
# Список объектов: (тип, название, координаты)
places = [
("city", "Moscow", (55.75, 37.62)),
("river", "Volga", (57.0, 41.0)),
("city", "Kazan", (55.79, 49.11)),
("mountain", "Elbrus", (43.35, 42.44)),
("river", "Ob", (55.0, 82.0)),
]
# Группируем по типу
places_by_type = {}
for place_type, name, coords in places:
# Если ключа ещё нет в словаре, то создаем его
if place_type not in places_by_type:
places_by_type[place_type] = []
# Добавляем значение к списку по ключу
places_list = places_by_type[place_type]
places_list.append({"name": name, "coords": coords})
# Либо сразу
# places_by_type[place_type].append({"name": name, "coords": coords})
print(places_by_type)
{'city': [{'name': 'Moscow', 'coords': (55.75, 37.62)}, {'name': 'Kazan', 'coords': (55.79, 49.11)}], 'river': [{'name': 'Volga', 'coords': (57.0, 41.0)}, {'name': 'Ob', 'coords': (55.0, 82.0)}], 'mountain': [{'name': 'Elbrus', 'coords': (43.35, 42.44)}]}
Методы множеств
С полным перечнем методов множеств и примерами их использования можно ознакомиться в справочном материале.
Методы словарей
С полным перечнем методов словарей и примерами их использования можно ознакомиться в справочном материале.
Практические задания