Заявление об ограничении ответственности. Хотя эта статья затрагивает несколько концепций, связанных с объектами словаря в 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__ были реализованы.

Интересно, правда?

Ну вот и все, ребята!

Надеюсь, это было информативно и весело.

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

Ссылки