Заявление об ограничении ответственности. Хотя эта статья затрагивает несколько концепций, связанных с объектами словаря в Python, она не является исчерпывающим руководством. Цель этой статьи - дать читателям базовое представление о том, как искать «ключи» в объектах словаря Python. Примеры фрагментов кода были разработаны и протестированы в Python 3.6.0.
Эта статья (данные, используемые для имени и адреса электронной почты в фрагментах кода) - произведение художественной литературы. Имена, персонажи, предприятия, места, события, места и происшествия являются продуктом воображения автора или используются вымышленным образом. Любое сходство с реальными людьми, живыми или мертвыми, или с реальными событиями чисто случайно.
(Лучше перестраховаться, чем сожалеть 😝)
Примечание. В конце этой статьи есть список ссылок для более подробного объяснения каждой отдельной концепции.
Что такое словарь в Python?
Словарь - это очень полезная и простая в использовании структура данных, поставляемая с Python. Даже если кто-то еще не сталкивался со словарями, в этой статье будут рассмотрены основные моменты, необходимые для понимания ключевой (каламбур?) Обсуждаемой здесь темы.
Словарь в Python - это неупорядоченный набор элементов, в котором каждый элемент хранится как пара ключ: значение. Обычно это объект сопоставления, который сопоставляет отдельные значения с уникальными ключами.
# Sample Dictionary friends = { "Steven": { "email": "[email protected]", "movies_watched": ["Batman: Arkham Knight", "Avengers: Infinity War"] }, "Claus": { "email": "[email protected]", "movies_watched": ["The Shawshank Redemption", "Harry Potter And the Goblet of Fire"] }, "Bridget": { "email": "[email protected]", "movies_watched": ["Inside Out", "How to Train Your Dragon"] }, "I": { "email": "[email protected]", } } # Fetching data from Dictionary # Fetching by key(like an index) claus_watched = friends[’Claus’][’movies_watched’] print(f’Claus Watched: {claus_watched}’) # .get allows us to define default values, in case key is missing you_watched = friends[’I’].get(’movies_watched’, ["And Justice For All"]) print(f’You Watched: {you_watched}’) # Output Claus Watched: ['The Shawshank Redemption', 'Harry Potter And the Goblet of Fire'] You Watched: ['And Justice For All']
Получение значений на основе ключей из словаря, как мы это делали в приведенном выше примере, называется поиском по ключу. Основная цель этой статьи - дать базовое объяснение того, как словари Python обрабатывают операцию поиска.
А что, если у кого-то есть два друга с одинаковыми именами? Ключи в словаре должны быть уникальными.
# Python supports different data types of keys # In below example, the data-type used for key is 'tuple'. friends[("Claus", "[email protected]")] = { "movies_watched": ["Star Wars", "Kung Fu Panda"] } print(friends) # Output - Cut out 'Steven' and 'Claus' to reduce space taken { 'Bridget': { 'email': '[email protected]', 'movies_watched': ['Inside Out', 'How to Train Your Dragon'] }, 'I': { 'email': '[email protected]' }, ('Claus', '[email protected]'): { 'movies_watched': ['Star Wars', 'Kung Fu Panda'] } }
Круто, похоже, Python допускает не только разные типы ключей, но и разные типы ключей в одном словаре.
[Я должен упомянуть, что приведенные здесь примеры просто демонстрируют способность Python обрабатывать различные типы данных для ключей. В реальных приложениях следует тщательно спланировать, как хранить данные.]
Но можем ли мы использовать любой допустимый тип данных Python в качестве словарного ключа?
# Let's try with list data-type as key friends[["Steven", "[email protected]"]] = { "movies_watched": ["A Star Is Born"] } Traceback (most recent call last): File "E:/Articles/Dictionary/code/medium.py", line 37, in <module> friends[["Steven", "[email protected]"]] = {"movies_watched": ["A Star Is Born"]} TypeError: unhashable type: 'list'
Чего ждать?!? Почему?!? Похоже, Python требует, чтобы ключи в словаре были хешируемыми (значения могут быть любым допустимым объектом Python и не разделяют это ограничение)
Errr, hashable?
Очень упрощенное определение Хеша: целое число фиксированного размера, которое вычисляется с использованием данных, хранящихся в объекте. Значения хеширования обладают следующими свойствами:
- Одинаковые данные будут иметь одинаковый хэш
- Любое изменение данных может (в идеале должно) также изменить хеш-значение.
- Поскольку данные могут быть любыми (фактическое содержимое и размер), два разных объекта (содержащих разные данные) могут иметь одно и то же хеш-значение. Это называется конфликтом хэша. Хороший алгоритм хеширования должен стараться минимизировать вероятность коллизии.
- В Python любой объект, имеющий реализацию для __hash __ (), считается хешируемым.
Что можно сделать?
В последнем примере мы пытались использовать объект list в качестве ключа Python. Давайте создадим наш собственный класс (потому что, как правило, мы никогда не должны изменять поведение встроенных типов данных Python, даже если некоторые хаки позволяют нам это сделать)
# Simple class class Friend: def __init__(self, name, email): self.name = name self.email = email
Теперь давайте попробуем использовать экземпляр этого класса в качестве ключа.
first_user_1 = Friend(name="Steven", email="[email protected]") sample_dictionary = { first_user_1: {"movies_watched": ["Batman: Arkham Knight", "Avengers: Infinity War"]} } print(sample_dictionary[first_user_1]["movies_watched"]) # Output ['Batman: Arkham Knight', 'Avengers: Infinity War']
В настоящее время мы не определили в нашем классе ничего особенного (__hash __ ()), которое могло бы сообщить Python, является ли он хешируемым или нет. Все классы наследуются от базового класса object Python, откуда и берется информация. Давай проверим
# In Python, all class by default inherits from 'object', which is the base built-in class. Can be verified by: print(Friend.__bases__) # dir - method tries to return a list of all attributes of the object print(dir(Friend)) # Output (<class 'object'>,) -- Output of print(Friend.__bases__) ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] -- Output of print(dir(Friend))
В списке атрибутов присутствует __hash__. Python проверяет наличие этого атрибута в методе, и если он не установлен, объект считается нехешируемым.
Давай попробуем еще раз поиск
Примечание. Для будущих операций не изменяйте содержимое словаря и обновляйте код «на месте». Отрывки не содержат определения словаря и других операторов, чтобы уменьшить размер этой статьи.
# Let's have another instance with same values for name and email first_user_2 = Friend(name="Steven", email="[email protected]") print(sample_dictionary[first_user_2]["movies_watched"]) # Output Traceback (most recent call last): File "E:/Articles/Dictionary/code/medium.py", line 68, in <module> print(sample_dictionary[first_user_2]["movies_watched"]) KeyError: <__main__.Friend object at 0x001DA3F0>
Почему не удалось выполнить поиск с помощью first_user_2? Метод хеширования по умолчанию, который поступает из класса object, вычисляет значение хеш-функции на основе идентификатора (id) каждого объекта (каждый объект имеет свой собственный уникальный идентификатор в Python. Однако вся концепция «каждого объекта» сложна и выходит за рамки данной статьи) Следовательно, хотя first_user_1 и first_user_2 содержат одни и те же данные, словарь поиск с использованием first_user_2 завершится ошибкой. Давайте проверим идентификаторы и хеш-значения двух объектов.
print(f"ID for first_user_1 {id(first_user_1)}") # hash - Method calls __hash__ attribute of the object print(f"Hash for first_user_1 {hash(first_user_1)}") print(f"ID for first_user_1 {id(first_user_2)}") print(f"Hash for first_user_1 {hash(first_user_2)}") # Output ID for first_user_1 30385168 Hash for first_user_1 1899073 ID for first_user_1 30385136 Hash for first_user_1 1899071
Определение пользовательского хеша
Пользовательский метод хеширования может быть добавлен путем реализации __hash __ ()
# Updating the class with __hash__ class Friend: def __init__(self, name, email): self.name = name self.email = email def __hash__(self): # Computing the hash value on name and email attribute print("IN __hash__") return hash((self.name, self.email)) # Now let's try first_user_2 = Friend(name="Steven", email="[email protected]") print(sample_dictionary[first_user_2]["movies_watched"]) # Output IN __hash__ Traceback (most recent call last): File "E:/Articles/Dictionary/code/medium.py", line 69, in <module> IN __hash__ print(sample_dictionary[first_user_2]["movies_watched"]) KeyError: <__main__.Friend object at 0x0024A3F0>
Причина, по которой вышеуказанная операция не удалась, связана с проверкой равенства, выполняемой Python во время поиска ключа словаря в дополнение к проверке хэша.
Почему выполняется проверка на равенство?
По умолчанию метод hash () в Python усекает возвращаемое значение пользовательского __hash __ () до 8 байтов. (64-битная система) или 4 байта (32-битная система) (хэш с потерями). Это может привести к конфликту хешей, что означает, что два разных объекта могут возвращать одно и то же хеш-значение. Чтобы предотвратить случайное совпадение ключа, помимо выполнения проверки хэша, во время поиска ключа также выполняется проверка равенства (потому что, если два объекта равны, их хеш-значения одинаковы - обратное значение не всегда верно, поскольку два объекта могут быть разными, но возвращать одинаковое хеш-значение)
Давайте определим собственный метод проверки равенства
Определение настраиваемой проверки равенства
Пользовательская проверка равенства может быть добавлена путем реализации __eq__
# Updating the class with __eq__ class Friend: def __init__(self, name, email): self.name = name self.email = email def __hash__(self): # Computing the hash value on name and email attribute print("IN __hash__") return hash((self.name, self.email)) def __eq__(self, other): print("IN __eq__") return self.name==other.name and self.email==other.email # Executing again first_user_2 = Friend(name="Steven", email="[email protected]") print(sample_dictionary[first_user_2]["movies_watched"]) # Output E:/Articles/Dictionary/code/medium.py" IN __hash__ IN __hash__ IN __eq__ ['Batman: Arkham Knight', 'Avengers: Infinity War']
В Python, если класс реализует __hash __ (), рекомендуется также реализовать __eq __ ().
Также, если класс реализует пользовательский метод __eq __ (), его __hash__ автоматически получает значение None (что делает его нехешируемым), если также не был реализован пользовательский метод __hash () __.
После успешной проверки хэша Python выполняет некоторую проверку идентичности (фактический объект, используемый в качестве ключа при определении словаря по сравнению с объектом, используемым для поиска) перед проверкой равенства. Если проверка личности прошла успешно, __eq__ не вызывается. Вот почему следующая операция завершается успешно:
# Update the class with intent to fail equality check class Friend: def __init__(self, name, email): self.name = name self.email = email def __hash__(self): # Computing the hash value on name and email attribute print("IN __hash__") return hash((self.name, self.email)) def __eq__(self, other): print("IN __eq__") return False # return self.name==other.name and self.email==other.email first_user_1 = Friend(name="Steven", email="[email protected]") sample_dictionary = { first_user_1: {"movies_watched": ["Batman: Arkham Knight", "Avengers: Infinity War"]} } print(f"first_user_1 {sample_dictionary[first_user_1]['movies_watched']}") # Output IN __hash__ IN __hash__ first_user_1 ['Batman: Arkham Knight', 'Avengers: Infinity War']
В приведенной выше операции __eq () __ не был вызван, иначе вывод содержал бы ‘IN __eq__’
А теперь давайте посмотрим на еще два фрагмента, просто для удовольствия.
first_user_1 = Friend(name="Steven", email="[email protected]") sample_dictionary = { first_user_1: {"movies_watched": ["Batman: Arkham Knight", "Avengers: Infinity War"]} } first_user_1.name = "Susan" print(f"first_user_1: {sample_dictionary[first_user_1]['movies_watched']}") # Output Traceback (most recent call last): File "E:/Articles/Dictionary/code/medium.py", line 63, in <module> print(f"first_user_1: {sample_dictionary[first_user_1]['movies_watched']}") IN __hash__ KeyError: <__main__.Friend object at 0x0041A410> IN __hash__
Значения хэша и изменяемые объекты - сложная задача. Хеш-значение не должно изменяться для объекта на протяжении всего его существования, и поскольку в пользовательском определении __hash__ значение хеш-функции было вычислено на основе имени и атрибута электронной почты, поиск в словаре выполняется с помощью first_user_1 завершается ошибкой, поскольку он был изменен с момента создания словаря [first_user_1.name был 'Steven'].
class Friend: def __init__(self, name, email): self.name = name self.email = email def __hash__(self): # Computing the hash value on name and email attribute print("IN __hash__") return hash((self.name, self.email)) def __eq__(self, other): print("IN __eq__") if(isinstance(other, Friend)): return self.name==other.name and self.email==other.email elif(isinstance(other, tuple)): return (self.name, self.email) == other else: return False first_user_1 = Friend(name="Steven", email="[email protected]") sample_dictionary = { first_user_1: {"movies_watched": ["Batman: Arkham Knight", "Avengers: Infinity War"]} } tuple_user = ("Steven", "[email protected]") print(f"Tuple User Works: f{sample_dictionary[tuple_user]['movies_watched']}") # Output IN __hash__ IN __eq__ Tuple User Works: f['Batman: Arkham Knight', 'Avengers: Infinity War']
Несмотря на то, что словарь был определен с типом данных Friend в качестве ключа и для типа данных lookup tuple, вышеуказанная операция завершается успешно из-за того, как __hash __ и __eq__ были реализованы.
Интересно, правда?
Ну вот и все, ребята!
Надеюсь, это было информативно и весело.
Пожалуйста, дайте мне знать о любых улучшениях или любых ошибках, которые могут присутствовать в вышеуказанном контенте 🙂
Ссылки
- Https://realpython.com/python-dicts/
- Https://www.journaldev.com/17357/python-hash-function
- Https://docs.python.org/3/reference/datamodel.html#object.__hash__
- Https://docs.python.org/3/reference/datamodel.html#object.__eq__
- Https://hynek.me/articles/hashes-and-equality/
- Https://www.asmeurer.com/blog/posts/what-happens-when-you-mess-with-hashing-in-python/
- Https://rszalski.github.io/magicmethods/#reflection