Хочете словник?


156

Для кешування потрібно генерувати кеш-ключ із аргументів GET, які присутні в диктаті.

В даний час я використовую sha1(repr(sorted(my_dict.items())))( sha1()це зручний метод, який використовує хешліб всередині), але мені цікаво, чи є кращий спосіб.


4
це може не працювати з вкладеним диктом. найкоротшим рішенням є натомість використовувати json.dumps (my_dict, sort_keys = True), який буде повторюватися у значення dict.
Андрій Федоров

2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 говорить: "Вихід з соління не гарантується канонічним з аналогічних причин, щоб диктувати та встановлювати порядок недетермінованим. Не використовуйте соління або pprint або repr для хешування. . "
Меттью Корнелл

сортувати клавіші dict, а не елементи, я також надішлю ключі до хеш-функції.
nyuwec

2
Цікава історія про хешування змінних структур даних (на зразок словників): python.org/dev/peps/pep-0351 було запропоновано для дозволу довільного заморожування об'єктів, але відхилено. Для обґрунтування дивіться цю тему в python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur

Якщо ваші дані у форматі json, і ви хочете семантично інваріантне хешування, оформити замовлення github.com/schollii/sandals/blob/master/json_sem_hash.py . Він працює на вкладених структурах (звичайно, починаючи з json) і не залежить від внутрішніх даних дикту, як збережений порядок (який розвивався протягом життя пітона), і дасть той самий хеш, якщо дві структури даних семантично однакові ( як {'a': 1, 'b':2}семантично те саме, що {'b':2, 'a':1}). Я ще не використовував його на чомусь надто складному, тому YMMV, але відгуки вітаються.
Олівер

Відповіді:


110

Якщо ваш словник не вкладений, ви можете зробити заморожений набір з елементами дикту та використовувати hash():

hash(frozenset(my_dict.items()))

Це набагато менш обчислювально, ніж генерування рядка JSON або представлення словника.

ОНОВЛЕННЯ: Будь ласка, дивіться коментарі нижче, чому такий підхід може не дати стабільного результату.


9
Це не спрацювало для мене із вкладеним словником. Я не спробував рішення нижче (занадто складне). Рішення ОП працює чудово. Я замінив sha1 хешем, щоб зберегти імпорт.
spatel

9
@Ceaser Це не спрацює, оскільки кортеж передбачає впорядкування, але елементи dict мають не упорядкований характер. заморожений набір краще.
Сурма

28
Остерігайтеся вбудованого хеша, якщо щось потрібно узгоджувати на різних машинах. Впровадження python на хмарних платформах, таких як Heroku та GAE, повертає різні значення для хеш () у різних примірниках, роблячи його непотрібним для будь-якого, що має бути поділене між двома або більше "машинами" (dynos у випадку heroku)
Бен Робертс

6
Це може бути цікаво, що hash()функція не дає стабільного виводу. Це означає, що, даючи один і той же вхід, він повертає різні результати з різними екземплярами одного і того ж інтерпретатора пітона. Мені це здається, що якесь значення насіння створюється щоразу, коли інтерпретатор запускається.
Герман Шахнер

7
очікуваний. насіння вводиться з міркувань безпеки, наскільки я пам'ятаю, щоб додати якусь рандомізацію пам'яті. Таким чином, ви не можете очікувати, що хеш буде однаковим між двома процесами python
Nikokrock

137

Використання sorted(d.items())недостатньо, щоб отримати стабільну репр. Деякі з значень у dможе бути і словниками, і їхні ключі все одно вийдуть у довільному порядку. Поки всі клавіші є рядками, я вважаю за краще використовувати:

json.dumps(d, sort_keys=True)

Однак, якщо хеші повинні бути стабільними на різних машинах або версіях Python, я не впевнений, що це непробито. Ви можете додати аргументи separatorsта ensure_asciiаргументи, щоб захистити себе від будь-яких змін у налаштуваннях за замовчуванням. Буду вдячний за коментарі.


6
Це просто параної, але JSON дозволяє більшості персонажів з'являтись у рядках без будь-якого буквального втечі, тому кодер може зробити якийсь вибір щодо того, чи слід уникати символів чи просто пропускати їх. Тоді ризик полягає в тому, що різні версії (або майбутні версії) кодера за замовчуванням можуть робити різні можливі варіанти, і тоді ваша програма обчислить різні значення хешу для одного і того ж словника в різних середовищах. ensure_asciiАргумент захищатиме проти цього абсолютно гіпотетичною проблеми.
Джек О'Коннор

4
Я перевірив роботу цього з різним набором даних, це набагато швидше , ніж make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujson не гарантує порядок дікт-пар, тому робити це не безпечно
arthurprs

11
Це рішення працює лише до тих пір, поки всі ключі є рядками, наприклад json.dumps ({'a': {(0, 5): 5, 1: 3}}).
kadee

5
@LorenzoBelli, ви можете подолати це, додавши default=strдо dumpsкоманди. Здається, добре працює.
mlissner

63

EDIT : Якщо всі ваші клавіші - це рядки , то перш ніж продовжувати читати цю відповідь, будь ласка, знайдіть значно простіше (і швидше) рішення Джека О'Коннора (яке також працює для хешування вкладених словників).

Незважаючи на те, що відповідь прийнято, назва питання "Заховання словника пітона", і відповідь неповна щодо цього заголовка. (Що стосується суті питання, відповідь є повною.)

Вкладені словники

Якщо ви шукаєте переповнення стека для того, як хеш-словник, можна наткнутися на це влучно під назвою питання і залишити незадоволеним, якщо намагається хеш помножити вкладені словники. Відповідь вище не працюватиме в цьому випадку, і вам доведеться реалізувати якийсь рекурсивний механізм, щоб отримати хеш.

Ось один з таких механізмів:

import copy

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that contains
  only other hashable types (including any lists, tuples, sets, and
  dictionaries).
  """

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

Бонус: Захоплення предметів та класів

Ця hash()функція чудово працює, коли у вас є хеш-класи або екземпляри. Однак ось одне питання, яке я знайшов із хешем щодо об’єктів:

class Foo(object): pass
foo = Foo()
print (hash(foo)) # 1209812346789
foo.a = 1
print (hash(foo)) # 1209812346789

Хеш - той самий, навіть після того, як я змінив foo. Це тому, що особа foo не змінилася, тому хеш - той самий. Якщо ви хочете, щоб Foo хешував по-різному, залежно від його поточного визначення, рішення - відмовитися від того, що насправді змінюється. У цьому випадку __dict__атрибут:

class Foo(object): pass
foo = Foo()
print (make_hash(foo.__dict__)) # 1209812346789
foo.a = 1
print (make_hash(foo.__dict__)) # -78956430974785

На жаль, коли ви намагаєтесь зробити те ж саме з самим класом:

print (make_hash(Foo.__dict__)) # TypeError: unhashable type: 'dict_proxy'

__dict__Властивість класу не є звичайним словником:

print (type(Foo.__dict__)) # type <'dict_proxy'>

Ось аналогічний механізм, як і попередній, який буде відповідним чином обробляти класи

import copy

DictProxyType = type(object.__dict__)

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that 
  contains only other hashable types (including any lists, tuples, sets, and
  dictionaries). In the case where other kinds of objects (like classes) need 
  to be hashed, pass in a collection of object attributes that are pertinent. 
  For example, a class can be hashed in this fashion:

    make_hash([cls.__dict__, cls.__name__])

  A function can be hashed like so:

    make_hash([fn.__dict__, fn.__code__])
  """

  if type(o) == DictProxyType:
    o2 = {}
    for k, v in o.items():
      if not k.startswith("__"):
        o2[k] = v
    o = o2  

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

Ви можете використовувати це для повернення хеш-коду з усіх бажаючих елементів:

# -7666086133114527897
print (make_hash(func.__code__))

# (-7666086133114527897, 3527539)
print (make_hash([func.__code__, func.__dict__]))

# (-7666086133114527897, 3527539, -509551383349783210)
print (make_hash([func.__code__, func.__dict__, func.__name__]))

ПРИМІТКА: всі наведені вище коди передбачають Python 3.x. Не тестував у попередніх версіях, хоча я припускаю, що make_hash()буде працювати у скажімо, 2.7.2. Що стосується того, щоб приклади працювали, я це знаю

func.__code__ 

слід замінити на

func.func_code

isin substance бере послідовність для другого аргументу, тому isinstance (o, (set, кортеж, список)) спрацює.
Xealot

дякую за те, що я зрозумів, що заморожений може послідовно параметри хеш-
запитів

1
Елементи потрібно сортувати, щоб створити один і той же хеш, якщо порядок елементів dict відрізняється, але ключові значення не -> return hash (кортеж (набір заморожених (сортування (new_o.items ())))))))
Bas Koopmans

Приємно! Я також додав дзвінок до hashсписків та кортежів. В іншому випадку він бере мої списки цілих чисел, які трапляються значеннями в моєму словнику, і повертає назад списки хешів, що не є тим, чого я хочу.
оса

Заморожений набір - це НЕПЕРЕВІРНА колекція, тому нічого не можна отримати, сортуючи його вхідні дані. Списки та кортежі, з іншого боку, - ЗАМОВЛЕННІ колекції ("послідовності"), і тому на значення хеша має впливати порядок елементів у межах. Ви не повинні їх сортувати!
RobM

14

Ось більш чітке рішення.

def freeze(o):
  if isinstance(o,dict):
    return frozenset({ k:freeze(v) for k,v in o.items()}.items())

  if isinstance(o,list):
    return tuple([freeze(v) for v in o])

  return o


def make_hash(o):
    """
    makes a hash out of anything that contains only list,dict and hashable types including string and numeric types
    """
    return hash(freeze(o))  

Якщо ви перейдете if isinstance(o,list):на, if isinstance(obj, (set, tuple, list)):ця функція може працювати на будь-якому об’єкті.
Пітер Шорн

10

Нижче наведений код уникає використання функції хеша () Python, оскільки він не надаватиме хешей, які відповідають послідовним перезавантаженням Python (див. Хеш-функція в Python 3.3 повертає різні результати між сесіями ). make_hashable()перетворить об'єкт в вкладені кортежі, а make_hash_sha256()також перетворить repr()хеш SHA256, кодований base64, кодованим.

import hashlib
import base64

def make_hash_sha256(o):
    hasher = hashlib.sha256()
    hasher.update(repr(make_hashable(o)).encode())
    return base64.b64encode(hasher.digest()).decode()

def make_hashable(o):
    if isinstance(o, (tuple, list)):
        return tuple((make_hashable(e) for e in o))

    if isinstance(o, dict):
        return tuple(sorted((k,make_hashable(v)) for k,v in o.items()))

    if isinstance(o, (set, frozenset)):
        return tuple(sorted(make_hashable(e) for e in o))

    return o

o = dict(x=1,b=2,c=[3,4,5],d={6,7})
print(make_hashable(o))
# (('b', 2), ('c', (3, 4, 5)), ('d', (6, 7)), ('x', 1))

print(make_hash_sha256(o))
# fyt/gK6D24H9Ugexw+g3lbqnKZ0JAcgtNW+rXIDeU2Y=

1
make_hash_sha256(((0,1),(2,3)))==make_hash_sha256({0:1,2:3})==make_hash_sha256({2:3,0:1})!=make_hash_sha256(((2,3),(0,1))). Це не зовсім рішення, яке я шукаю, але це приємний проміжний продукт. Я думаю додати type(o).__name__до початку кожного з кортежів, щоб застосувати диференціацію.
Poik

Якщо ви також хочете сортувати список:tuple(sorted((make_hashable(e) for e in o)))
Сурай

make_hash_sha256 () - приємно!
jtlz2

1
@Suraj Ви не хочете сортувати список до хешування, оскільки списки, які містять їх вміст у різних порядках, точно не є одним і тим же. Якщо порядок елементів не має значення, проблема полягає в тому, що ви використовуєте неправильну структуру даних. Ви повинні використовувати набір замість списку.
scottclowe

@scottclowe Це дуже правда. Дякуємо, що додали цю точку. Є два сценарії, де ви все ще хочете список (без конкретних потреб в замовленні) - 1. Список елементів, що повторюються. 2. Коли потрібно використовувати JSON безпосередньо. Оскільки JSON не підтримує "встановити" представлення.
Сурай

5

Оновлено з відповіді 2013 року ...

Жодна з наведених відповідей мені не здається надійною. Причиною є використання предметів (). Наскільки мені відомо, це виходить у машино-залежному порядку.

Як щодо цього?

import hashlib

def dict_hash(the_dict, *ignore):
    if ignore:  # Sometimes you don't care about some items
        interesting = the_dict.copy()
        for item in ignore:
            if item in interesting:
                interesting.pop(item)
        the_dict = interesting
    result = hashlib.sha1(
        '%s' % sorted(the_dict.items())
    ).hexdigest()
    return result

Чому, на вашу думку, має значення те, dict.itemsщо не повертається передбачувано упорядкований список? frozensetпіклується про це
glarrain

2
Набір, за визначенням, є не упорядкованим. Таким чином, порядок додавання об'єктів не має значення. Ви повинні усвідомити, що вбудована функція hashне переймається тим, як друкується вміст замороженого набору чи щось подібне. Перевірте його в декількох версіях машини та пітона, і ви побачите.
glarrain

Чому ви використовуєте додатковий виклик хеш () у value = hash ('% s ::% s'% (value, type (value))) ??
RuiDo

4

Щоб зберегти ключовий порядок, замість hash(str(dictionary))або hash(json.dumps(dictionary))я вважаю за краще швидке та брудне рішення:

from pprint import pformat
h = hash(pformat(dictionary))

Він працюватиме навіть для таких типів, як DateTimeі більше, які не піддаються JSON.


3
Хто гарантує, що pformat або json завжди використовують одне замовлення?
ThiefMaster

1
@TentistMaster, "Змінено у версії 2.5: Словники сортуються за клавішами до того, як відображатиметься дисплей; до 2,5 словник сортувався лише у тому випадку, якщо його відображення вимагало більше одного рядка, хоча це не було задокументовано." ( Docs.python. org / 2 / library / pprint.html )
Арель

2
Це не здається мені дійсним. Автори розуміють, що модулі pprint та pformat призначені для відображення, а не для серіалізації. Через це ви не повинні відчувати себе в безпеці припускаючи, що pformat завжди повертає результат, який трапляється в роботі.
Девід Сандерс

3

Ви можете використовувати сторонній frozendictмодуль, щоб заморозити дік і зробити його зручним.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Для обробки вкладених об'єктів ви можете:

import collections.abc

def make_hashable(x):
    if isinstance(x, collections.abc.Hashable):
        return x
    elif isinstance(x, collections.abc.Sequence):
        return tuple(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Set):
        return frozenset(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Mapping):
        return frozendict({k: make_hashable(v) for k, v in x.items()})
    else:
        raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

Якщо ви хочете підтримувати більше типів, використовуйте functools.singledispatch(Python 3.7):

@functools.singledispatch
def make_hashable(x):
    raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

@make_hashable.register
def _(x: collections.abc.Hashable):
    return x

@make_hashable.register
def _(x: collections.abc.Sequence):
    return tuple(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Set):
    return frozenset(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Mapping):
    return frozendict({k: make_hashable(v) for k, v in x.items()})

# add your own types here

Це не працює, наприклад, для dictз DataFrameоб'єктів.
Джеймс Гіршорн

@JamesHirschorn: Оновлено, щоб голосно вийти з ладу
Ерік

Краще! Я додав наступне elifзастереження, щоб воно працювало з DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) я відредагую відповідь і побачу, чи приймаєте ви її ...
Джеймс Гіршорн

1
ГАРАЗД. Я бачу, що я просив щось більше, ніж "хешируемое", що гарантує лише те, що рівні об'єкти мають однаковий хеш. Я працюю над версією, яка дасть однакове значення між тиражами, незалежною від версії python тощо.
Джеймс Гіршорн

1
hashрандомізація - це цілеспрямована функція безпеки, включена за замовчуванням у python 3.7.
Ерік

1

Для цього можна використовувати бібліотеку карт . Зокрема, карти.FrozenMap

import maps
fm = maps.FrozenMap(my_dict)
hash(fm)

Щоб встановити maps, просто виконайте:

pip install maps

Він також обробляє вкладений dictкорпус:

import maps
fm = maps.FrozenMap.recurse(my_dict)
hash(fm)

Відмова: Я є автором mapsбібліотеки.


Бібліотека не сортує список всередині диктату. А отже, це може створювати різні хеші. Існує можливість також сортувати список. Заморожений набір повинен допомогти, але мені цікаво, як би ви впоралися зі справою за допомогою вкладеного дикта, що містить список диктів. Оскільки дикти непридатні.
Сурай

1
@Suraj: він робить ручки вкладену структуру з допомогою .recurse. Див. Maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Замовлення в списках має семантичне значення, якщо ви хочете незалежність замовлення, ви можете перетворити свої списки в набори до виклику .recurse. Ви можете також скористатися list_fnпараметром, щоб .recurseвикористовувати іншу структуру даних, що підлягає застосуванню, ніж tuple(.eg frozenset)
Pedro Cattori

0

Один із способів наблизитись до проблеми - скласти кортеж елементів словника:

hash(tuple(my_dict.items()))

-8

Я роблю це так:

hash(str(my_dict))

1
Чи може хтось пояснити, що так не так у цьому методі?
mhristache

7
Словники @maximi не є стабільними, це порядок порядку (таким чином, hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))хоча це може працювати для деяких словників, він не гарантовано працює над усіма).
Влад Фролов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.