Skip to content

Latest commit

 

History

History
682 lines (481 loc) · 34.2 KB

lesson04.md

File metadata and controls

682 lines (481 loc) · 34.2 KB

Лекция 4. Хэш таблицы. Set, frozenset. Dict. Tuple. Немного об импортах. Namedtuple, OrderedDict

Оглавление курса

Блок 1 — Python Basic (1–6)
Блок 2 — Git (7–8)
Блок 3 — Python Advanced (9–14)
Блок 4 — SQL (15–17)
Блок 5 — Django (19–26)
Блок 6 — Django Rest Framework (27–30)
Блок 7 — Python async (31–33)
Блок 8 — Deployment (34–35)

drawing

Да здравствуют новые типы данных!

Tuple, он же кортеж

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

По сути, это ещё одна коллекция, у которой есть свои особенности.

# Создаём tuple из разнородных элементов
tuple_different_types = (1, 'abc', True)

# То же самое без скобок
tuple_different_types_another = 1, 'abc', True

# Создаём tuple из однотипных элементов
tuple_same_types = (1, 2, 3)

# Присваиваем трём переменным элементы tuple со скобками и без
(a, b, c) = tuple_different_types
print(a, b, c)
# 1 'abc' True

# То же самое без скобок
z, y, x = tuple_different_types

# Создаём tuple из одного элемента
one_element_tuple = 12,
# (12,)

# преобразование списка в кортеж
my_list = [1, 2, 3, 4]
my_tuple = tuple(my_list)
# Внимание: в кортеж можно преобразовать любой итерируемый объект.

К кортежам применимы многие функции из тех, что применимы к спискам: получение длины кортежа, конкатенация (склеивание) кортежей, срезы, методы index и count:

my_tuple = 1, 2, 3
my_tuple = my_tuple + (4, 5)
# (1, 2, 3, 4, 5)

my_tuple += 6,
# (1, 2, 3, 4, 5, 6)

# Применимы срезы

my_tuple[:-1]
# (1, 2, 3, 4, 5)

my_tuple[2:-1]
# (3, 4, 5)

len(my_tuple)
# 6

my_tuple.index(2)
# 1

my_tuple.count(3)
# 1

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

Чаще всего кортежи используют для получения данных из функции, хранения каких-то неизменяемых данных и т. п. Чем привлекательна работа с ними:

  • работа с ними быстрее (по сравнению со списками);
  • занимают в памяти меньше места;
  • могут выступать в качестве ключей для словаря (если не содержат нехешируемых значений) — о словарях чуть ниже;
  • имеют только два метода: count и index;
  • используются для массовой инициализации переменных и возврата сразу нескольких значений из функции.

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

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

a = 100
b = 200
a, b = b, a

Хэш таблицы

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

Для этого нам необходимо познакомиться с некоторыми терминами и понятиями.

Сначала нужно понять, что такое хэш.

Хэш (hash)

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

Но мы туда не полезем :)

Что нам нужно знать про хеш? Нужно знать, что это функция, которая преобразовывает данные так, что их невозможно восстановить, но при этом одни и те же данные всегда превратятся в конкретное значение.

Условное A всегда превращается в B.

Из A получить B легко.

Из B получить A практически невозможно!

Если ваши данные — это кусок мяса, то хеш-функция — это мясорубка, которая превратит его в фарш. А фарш, как известно, невозможно прокрутить назад.

drawing

Что важно:

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

В Python функция hash встроена и работает без нашего участия.

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

hash(1)
# 1
hash('abc')
# -1860157324224119549
hash([1, 2, 3])
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: unhashable type: 'list'

Цифры до определённого размера будут преобразовываться сами в себя, но это нормально — не обращаем внимания.

Свойства хеш-функции используются также для хранения паролей, о чём мы поговорим, когда до этого дойдём.

Хэш таблицы

Зачем нам эта информация?

Потому что в Python, да и в других языках программирования, существует довольно много структур хранения данных, которые основываются на хеш-таблицах.

Что такое хэш-таблицы?

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

Как это работает под капотом?

drawing

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

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

Set (Множество)

Наконец к сути.

Первый тип данных, основанный на хэш-таблицах, — это set, он же множество. Он является реализацией теории множеств из дискретной математики, если кто-то её когда-то учил :)

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

В этом определении упомянуты три основные особенности сетов — изменяемость, уникальность и отсутствие сортировки.

Почему так?

Под капотом сет — это хэш-таблица, где в качестве значения записывается само значение объекта. А это значит, что если мы попытаемся записать в сет дублирующее значение, мы просто перезапишем ту же самую строку хэш-таблицы.

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

Изменяемость — в таблицу можно добавить или удалить элементы, а значит, таблица изменится.

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

Уникальность: сет содержит только уникальные элементы. Если добавлять в него дубликаты — они не добавляются; если перевести список в сет — дублирующие элементы будут удалены.

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

new_set = {1, 2, 3, 4, 5, 4, 3, 4, 5, 6, 5, 4, 3}

# {1, 2, 3, 4, 5, 6}

another_set = {1, 2, 3, 'a', 'c', 0.34}

# {0.34, 1, 2, 3, 'a', 'c'}

incorrect_set = {1, 2, [1, 2]}
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: unhashable type: 'list'

Множества поддерживают некоторые операции:

Разность множеств — оператор -. Возвращает множество элементов, которые есть в первом множестве, но нет во втором.

Объединение множеств — оператор |. Возвращает нам множество, которое содержит все элементы, которые есть в первом множестве и во втором.

Пересечение множеств — оператор &. Возвращает нам множество, которое содержит все элементы, которые есть и в первом множестве, и во втором.

set1 = {1, 2, 3, 4, 5, 6}
set2 = {5, 6, 7, 8, 9}
set1 - set2  # Разность множеств
# {1, 2, 3, 4}

set1 | set2  # Объединение множеств
# {1, 2, 3, 4, 5, 6, 7, 8, 9}

set1 & set2  # Пересечение множеств
# {5, 6}

Также есть симметрическая разница — оператор ^ (или метод symmetric_difference). Возвращает элементы, которые есть ровно в одном из множеств:

set1 ^ set2  # Симметрическая разница
# {1, 2, 3, 4, 7, 8, 9}

Добавить элемент в множество можно при помощи метода add, а удалить из множества элемент — при помощи метода remove. В качестве параметра выступает сам элемент, поскольку индексов в множестве нет.

set1.add(7)

# {1, 2, 3, 4, 5, 6, 7}

set1.remove(1)
# {2, 3, 4, 5, 6, 7}

# При добавлении дубликата проблем не возникнет, но и эффекта не будет

set1.add(5)
# {2, 3, 4, 5, 6, 7}

# При удалении несуществующего объекта выпадет ошибка
set1.remove('a')
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# KeyError: 'a'

Совет: чтобы удалять без ошибки, используйте discard() — он не вызывает KeyError, если элемента нет:

set1.discard('a')  # ошибки не будет, даже если 'a' отсутствует

Сеты можно использовать для фильтрации дубликатов в коллекциях. Для этого коллекцию нужно преобразовать в сет, а потом обратно, используя функции list() и set():

list_with_duplicates = [1, 2, 3, 4, 3, 2, 5, 6, 7, 5, 3, 2]
# [1, 2, 3, 4, 3, 2, 5, 6, 7, 5, 3, 2]
list_without_duplicates = list(set(list_with_duplicates))
# [1, 2, 3, 4, 5, 6, 7]

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

programmers = {'ivanov', 'petrov', 'sidorov'}
managers = {'ivanov', 'moxov', 'goroxov'}
# И программист, и менеджер:
programmers & managers
# {'ivanov'}
# Все, и программисты, и менеджеры:
programmers | managers
# {'ivanov', 'petrov', 'sidorov', 'goroxov', 'moxov'}
# Программисты, которые не менеджеры:
programmers - managers
# {'petrov', 'sidorov'}

frozenset

Также существует специальный тип данных, который превращает сет в неизменяемый тип данных — он называется frozenset:

fs = frozenset([1, 2, 3, 4, 3, 2, 1])
# frozenset({1, 2, 3, 4})

Практика

  1. Создать два множества, в которых есть общие элементы. Найти: какие элементы есть в обоих множествах, какие есть только в первом, какие только во втором. Найти множество, в котором будут элементы из обоих множеств.
  2. Создать список с повторениями, удалить повторения.
  3. Создать два списка. Написать код, который будет печатать True, если в списках есть общие элементы, и False — если нет.

Dict (Словарь)

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

Если set был упрощённым использованием хеш-таблиц, то словарь является их полной реализацией.

Значением элемента словаря может быть любой тип данных, ключом — любой хэшируемый тип данных: str, int, float, tuple (с ограничениями, об этом дальше) и пр.

Ключом могут быть те же самые объекты, которые могут быть использованы в сете.

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

Есть несколько способов создать словарь: прямое создание, создание при помощи преобразования в тип (используя функцию dict), используя функцию fromkeys и через генератор словарей.

Рассмотрим все эти способы на примере:

d = {}  # Создание пустого словаря напрямую. Это не set; чтобы создать пустой set, используйте s = set()

# {}
d1 = {'a': 1, 'b': 2}  # Создание словаря напрямую

# {'a': 1, 'b': 2}

# создание словаря при помощи функции dict, она же используется для приведения типов:
d = dict(short='dict', long='dictionary')
# {'short': 'dict', 'long': 'dictionary'}

d = dict([(1, 1), (2, 4)])
# {1: 1, 2: 4}

# создание словаря при помощи функции fromkeys:
d = dict.fromkeys(['a', 'b', 1, (1, 2)])
# {'a': None, 1: None, 'b': None, (1, 2): None}

# с заполнением одним значением
d = dict.fromkeys(['a', 'b', 1, (1, 2)], 4)
# {'a': 4, 1: 4, 'b': 4, (1, 2): 4}

# создание словаря при помощи генератора словарей (dict comprehension, по аналогии со списками) :
d = {a: a ** 2 for a in range(7)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}

У функции dict есть одна особенность: с её помощью можно быстро создавать словари с ключами-строками, опуская кавычки. Это показано в примере ниже. К сожалению, работает только с явными строками, принцип формирования которых такой же, как и принцип наименования переменных:

dict(a=1, b=2, c=3, d=13)
# {'a': 1, 'c': 3, 'b': 2, 'd': 13}
dict(a=1, b=2, c=3, d=13, 1 = 2)
# File "<stdin>", line 1
# SyntaxError: keyword can't be an expression

Операции со словарями

Со словарями доступны операции взятия элемента, удаления элемента, добавления элемента и его обновления:

d = dict(a=1, b=2, c=3, d=13)
# {'a': 1, 'c': 3, 'b': 2, 'd': 13}
d['a']
# 1
d[1] = 15
# {'a': 1, 1: 15, 'c': 3, 'b': 2, 'd': 13}
del d[1]
# {'a': 1, 'c': 3, 'b': 2, 'd': 13}
d['a'] = 111
d['a']
# 111

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

По ключу - небезопасный вариант

Через get - безопасный

Это не значит, что один правильный, а второй нет — это значит, что они по-разному работают.

d['a']
# 1
d['e']
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# KeyError: 'e'
d.get('e')
# Ничего не отпишет, но там будет None
d.get('e', 'No such element')
# 'No such element'

Методы и функции для работы со словарями

# Добавление элементов из другого словаря
d
# {'a': 1, 'c': 3, 'b': 2, 'd': 13}
d.update({'4': 4, '5': 5})
# {'a': 1, 'c': 3, 'b': 2, '5': 5, 'd': 13, '4': 4}

# Количество пар в словаре
len(d)
# 6

d.keys()  # Представление ключей
# dict_keys(['a', 'c', 'b', '5', 'd', '4'])
d.values()  # Представление значений
# dict_values([1, 3, 2, 5, 13, 4])
d.items()  # Получить список элементов — кортежей
# dict_items([('a', 1), ('c', 3), ('b', 2), ('d', 13), ('4', 4), ('5', 5)])

Словари прекрасно проходятся в цикле:

for key in d:  # По умолчанию перебор словаря идет по ключам
    print(key, d[key])
for key, val in d.items():  # проход по парам (используем кортежи)
    print(key, val)

# При переборе самого объекта будет вызван перебор ключей.

Практика

  1. Перевернуть словарь (было {1: 2, 3: 4}, стало {2: 1, 4: 3}). После этого сделать то же самое через dict comprehension.
  2. Есть два списка одинаковой длины. Создать словарь, где ключи — это элементы первого списка, а значения — элементы второго. Например: [1, 2, 3] и [4, 5, 6], результат — {1: 4, 2: 5, 3: 6}.
  3. Есть строка с предложением, в котором есть повторяющиеся слова. Создать словарь, где ключи — это слова из предложения, а значение — количество раз, которое встречается это слово. Пример: "привет я хочу привет я", результат — {"привет": 2, "я": 2, "хочу": 1}.

Enumerate

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

enumerate(iterable, start=0)

Где:

  • iterable — итерируемый объект, который вы хотите перебирать.
  • start — начальный индекс, с которого будет начинаться счётчик (по умолчанию 0).

Функция enumerate возвращает объект, который при итерации возвращает кортежи, каждый из которых содержит индекс и соответствующее значение из итерируемого объекта.

Пример:

fruits = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits):
    print(f"Index {index}: {fruit}")

Вывод:

Index 0: apple
Index 1: banana
Index 2: cherry

Пример со сдвигом старта:

fruits = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits, start=1):
    print(f"Index {index}: {fruit}")

Вывод:

Index 1: apple
Index 2: banana
Index 3: cherry

Импорты и некоторые специфические типы данных

Импорты

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

Одним из инструментов, позволяющих это сделать, являются ключевые слова import и from, которые позволяют «подключить» дополнительный функционал.

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

Также можно импортировать модули после установки дополнительных библиотек, но об этом позже — у вас будет целая лекция, посвящённая тому, как разбивать код на модули, импортировать и называть их.

Как мы можем что-либо импортировать?

Ключевые слова import и from, если нам нужно импортировать весь модуль, мы просто пишем

import base64  # base64 — это просто название одного из модулей

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

from math import sqrt  # импортируем только функцию sqrt из модуля math

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

from datetime import datetime as dt

dt.now()
# datetime.datetime(2023, 2, 27, 16, 6, 32, 679427)

Что же мы можем импортировать?

namedtuple

Тип данных, который работает как кортеж, но позволяет «именовать значения», например:

from collections import namedtuple

Student = namedtuple('Student', ['name', 'age', 'DOB'])
s = Student('Nandini', '19', '2541997')
s.name
# Nandini
s.age
# '19'

deque

Двухсторонняя очередь — ещё один тип данных:

from collections import deque

d = deque()
d.append('1')
d.appendleft('3')
d
# deque(['3', '1'])

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

OrderedDict

Исторически — словарь, который гарантировал порядок ключей. Начиная с Python 3.7 обычные dict сохраняют порядок вставки; OrderedDict полезен для дополнительных методов (например, move_to_end).

from collections import OrderedDict

d = OrderedDict(a=1, b=2)
# OrderedDict([('a', 1), ('b', 2)])

Существует ещё множество разных модулей — вы будете знакомиться с ними по мере необходимости. Вряд ли найдётся человек, который знает все стандартные модули, а тем более те, которые можно установить дополнительно.

Практика

  1. Создать словарь оценок студентов (ключ — ФИ студента, значение — список оценок). Найти самого успешного и самого отстающего по среднему баллу.
  2. Создать структуру данных для студентов из имён и фамилий (можно придумать любые). Придумать структуру данных, чтобы указывать, в какой группе учится студент (группы: Python, FrontEnd, FullStack, Java). Студент может учиться в нескольких группах. Затем вывести:
    • студентов, которые учатся в двух и более группах;
    • студентов, которые не учатся на FrontEnd;
    • студентов, которые изучают Python или Java.

← Лекция 3: None. Range, list comprehension, sum, max, min, len, sorted, all, any. Работа с файлами | Лекция 5: Функции, типизация, lambda. Map, zip, filter. →