Поймите, как объекты Python ведут себя как хешированные ключи словаря.

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

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

{'Apple': 1, 'Apple': 2, 'Apple': 3}

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

Ключи должны быть хешируемыми

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

Давайте создадим собственный класс, реализующий метод __hash__(). Таким образом, он хешируется, и мы можем использовать его объекты в качестве ключей словаря.

class Fruit:
    
    def __hash__(self):
        return 1
    
d = {
    Fruit(): 1,
    Fruit(): 2
}

# Printing d gives us the following
# {<__main__.Fruit at 0x10873eb10>: 1, <__main__.Fruit at 0x10873d9d0>: 2}

Метод __hash__() возвращает постоянное значение, 1. Т.е. оба экземпляра Fruit имеют одинаковый хэш. Тем не менее, в итоге получается два отдельных ключа. Почему?

Вы, вероятно, знаете следующее о хеш-функциях:

Одни и те же значения имеют одинаковые выходные данные, но наличие одинаковых выходных данных не гарантирует, что входные данные будут одинаковыми.

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

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

Есть идеи, что попробовать дальше?

Ключи должны быть уникальными

Давайте также переопределим метод __eq__() и убедимся, что наши плоды равны.

class Fruit:
    
    def __hash__(self):
        return 1
    
    def __eq__(self, other):
        return True
    
d = {
    Fruit(): 1,
    Fruit(): 2
}

# This time, printing d gives us the following
# {<__main__.Fruit at 0x10873ed50>: 2}

Тада! Наконец-то у нас есть одинаковые хеши и одинаковые ключи, и теперь оба ключа считаются одинаковыми. т.е. второй перевешивает первый.

Конечно, мы можем программно решить, хотим ли мы, чтобы все было равным или нет.

class Fruit:
    
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name) 
    
    def __eq__(self, other):
        return self.name == other.name
    
d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,
    Fruit("Apple"): 3
}

# This time, printing d gives us the following
# {<__main__.Fruit at 0x10873cb90>: 3, <__main__.Fruit at 0x108247ad0>: 2}

Одевание ключей

Но ключи выглядят некрасиво, нам не нравится эта штука <__main__.Fruit .. blah blah. Можем ли мы это изменить?

Конечно, методом__repr__().

class Fruit:
    
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name) 
    
    def __eq__(self, other):
        return self.name == other.name
    
    def __repr__(self):
        return f"'{self.name}'"
    
d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,
    Fruit("Apple"): 3
}

# And now, printing d gives us the following
# {'Apple': 3, 'Orange': 2}

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

Все, что нам нужно, — это класс, в котором объекты имеют разные хэши, но их представления одинаковы. Что-то вроде этого:

class Fruit:
    
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name) 
    
    def __eq__(self, other):
        return self.name == other.name
    
    def __repr__(self):
        return "'Apple'"
    
d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,
    Fruit("Banana"): 3
}

# And now, printing d gives us the following
# {'Apple': 1, 'Apple': 2, 'Apple': 3}

На самом деле то, что я только что сделал, — это откровенное зло.

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

И они получат ошибку, если попытаются сделать что-то вроде этого:

d['Apple']

Добавьте к этому, что хотя 3 ключа выглядят одинаково, они не одинаковы.

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

Кстати, если класс не реализует методы __hash__() и __eq__(), Python предоставляет реализацию по умолчанию, в которой объекты не равны.

Определяемые пользователем классы по умолчанию имеют методы __eq__() и __hash__(); с ними все объекты сравниваются неравно (кроме самих себя), и x.__hash__() возвращает соответствующее значение, так что x == y подразумевает как x is y, так и hash(x) == hash(y)”. [«Документация по Python]

И есть причина, по которой Python хочет, чтобы ключи словаря были неизменяемыми. Или же можно сделать следующее:

class Fruit:
    
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name) 
    
    def __eq__(self, other):
        return self.name == other.name
    
    def __repr__(self):
        return self.name
    
apple = Fruit("Apple")
orange = Fruit("Orange")

d = {
    apple: 1,
    orange: 2,
}

Теперь, если вы мутируете apple, у вас возникнут проблемы.

apple.name = "banana"
d[apple]

# This will complain raising a KeyError
# KeyError: banana

Как я уже сказал, этот пост предназначен только для образовательных целей.

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

И прежде чем вы уйдете, вот еще один пост о взломе вещей



И если вы еще не являетесь участником Medium и не можете прочитать эту и многие другие статьи, вы можете присоединиться, используя мою реферальную ссылку ниже.