Memukul kamus?

156

Untuk tujuan caching saya perlu membuat kunci cache dari argumen GET yang ada dalam dikt.

Saat ini saya menggunakan sha1(repr(sorted(my_dict.items())))( sha1()adalah metode kenyamanan yang menggunakan hashlib secara internal) tapi saya ingin tahu apakah ada cara yang lebih baik.

Pencuri
sumber
4
ini mungkin tidak bekerja dengan dict bersarang. solusi terpendek adalah menggunakan json.dumps (my_dict, sort_keys = True) sebagai gantinya, yang akan berulang menjadi nilai dict.
Andrey Fedorov
2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 mengatakan "Output dari acar tidak dijamin kanonik karena alasan yang sama dengan dict dan mengatur pesanan menjadi non-deterministik. Jangan gunakan acar atau cetak atau repr untuk hashing . "
Matthew Cornell
urutkan kunci dict, bukan item, saya juga akan mengirim kunci ke fungsi hash.
nyuwec
2
Latar belakang yang menarik tentang hashing struktur data yang bisa berubah-ubah (seperti kamus): python.org/dev/peps/pep-0351 diusulkan untuk memungkinkan objek yang dibekukan secara sewenang-wenang, tetapi ditolak. Untuk alasannya, lihat utas ini di python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur
Jika data Anda berformat json, dan Anda ingin hashing semantik invarian, checkout github.com/schollii/sandals/blob/master/json_sem_hash.py . Ia bekerja pada struktur bersarang (tentu saja, sejak json), dan tidak bergantung pada internal dict seperti pesanan yang diawetkan (yang telah berevolusi selama masa python), dan akan memberikan hash yang sama jika dua struktur data secara semantik sama ( suka {'a': 1, 'b':2}secara semantik sama dengan {'b':2, 'a':1}). Saya belum menggunakannya pada sesuatu yang terlalu rumit namun YMMV tetapi umpan balik diterima.
Oliver

Jawaban:

110

Jika kamus Anda tidak bersarang, Anda bisa membuat frozenset dengan item dict dan menggunakan hash():

hash(frozenset(my_dict.items()))

Ini jauh lebih sedikit komputasi daripada menghasilkan string JSON atau representasi kamus.

UPDATE: Silakan lihat komentar di bawah ini, mengapa pendekatan ini mungkin tidak menghasilkan hasil yang stabil.

Imran
sumber
9
Ini tidak berfungsi untuk saya dengan kamus bersarang. Saya belum mencoba solusi di bawah ini (terlalu rumit). Solusi OP bekerja dengan sangat baik. Saya mengganti sha1 dengan hash untuk menyimpan impor.
spatel
9
@Ceaser Itu tidak akan berfungsi karena tuple menyiratkan pemesanan tetapi item-item dict tidak diurutkan. frozenset lebih baik.
Antimony
28
Waspadalah terhadap hash bawaan jika ada sesuatu yang harus konsisten di berbagai mesin. Implementasi python pada platform cloud seperti Heroku dan GAE akan mengembalikan nilai yang berbeda untuk hash () pada contoh yang berbeda menjadikannya tidak berguna untuk apa pun yang harus dibagi antara dua atau lebih "mesin" (dinamika dalam kasus heroku)
Ben Roberts
6
Mungkin menarik hash()fungsi tidak menghasilkan output yang stabil. Ini berarti bahwa, dengan input yang sama, ia mengembalikan hasil yang berbeda dengan contoh yang berbeda dari juru bahasa python yang sama. Bagi saya, sepertinya semacam nilai seed dihasilkan setiap kali penerjemah dimulai.
Hermann Schachner
7
diharapkan. benih diperkenalkan untuk alasan keamanan sejauh yang saya ingat untuk menambahkan beberapa jenis pengacakan memori. Jadi Anda tidak dapat mengharapkan hash sama antara dua proses python
Nikokrock
137

Menggunakan sorted(d.items())tidak cukup untuk membuat kita repr stabil. Beberapa nilai dalam dbisa berupa kamus juga, dan kunci-kuncinya masih akan keluar dalam urutan yang sewenang-wenang. Selama semua kunci adalah string, saya lebih suka menggunakan:

json.dumps(d, sort_keys=True)

Yang mengatakan, jika hash harus stabil di berbagai mesin atau versi Python, saya tidak yakin ini anti peluru. Anda mungkin ingin menambahkan separatorsdan ensure_asciiargumen untuk melindungi diri dari perubahan apa pun ke default di sana. Saya menghargai komentar.

Jack O'Connor
sumber
6
Ini hanya menjadi paranoid, tetapi JSON memungkinkan sebagian besar karakter muncul dalam string tanpa ada pelarian secara literal, sehingga pembuat enkode dapat membuat beberapa pilihan apakah akan keluar dari karakter atau hanya meneruskannya. Resikonya kemudian adalah bahwa versi yang berbeda (atau versi masa depan) dari pembuat kode dapat membuat pilihan yang berbeda untuk melarikan diri secara default, dan kemudian program Anda akan menghitung nilai hash yang berbeda untuk kamus yang sama di lingkungan yang berbeda. The ensure_asciiArgumen akan melindungi terhadap masalah yang sama sekali hipotetis ini.
Jack O'Connor
4
Saya menguji kinerja ini dengan dataset yang berbeda, ini jauh lebih cepat daripada make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax
3
@charlax ujson tidak menjamin urutan pasangan dikt, jadi tidak aman untuk melakukannya
arthurprs
11
Solusi ini hanya berfungsi selama semua kunci adalah string, mis. Json.dumps ({'a': {(0, 5): 5, 1: 3}}) gagal.
kadee
5
@LorenzoBelli, Anda dapat mengatasi itu dengan menambahkan default=strke dumpsperintah. Tampaknya bekerja dengan baik.
mlissner
63

EDIT : Jika semua kunci Anda adalah string , maka sebelum melanjutkan untuk membaca jawaban ini, silakan lihat solusi Jack O'Connor yang secara signifikan lebih sederhana (dan lebih cepat) (yang juga berfungsi untuk membuat kamus bersarang bersarang).

Meskipun jawaban telah diterima, judul pertanyaannya adalah "Hashing a python dictionary", dan jawabannya tidak lengkap sehubungan dengan judul itu. (Sehubungan dengan tubuh pertanyaan, jawabannya sudah lengkap.)

Kamus bersarang

Jika seseorang mencari Stack Overflow untuk bagaimana mem-hash kamus, orang mungkin akan tersandung pada pertanyaan yang berjudul tepat ini, dan membiarkannya tidak puas jika seseorang mencoba untuk meng hash kamus berlipat ganda yang bersarang. Jawaban di atas tidak akan berfungsi dalam kasus ini, dan Anda harus menerapkan semacam mekanisme rekursif untuk mengambil hash.

Berikut ini salah satu mekanismenya:

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()))))

Bonus: Objek dan Kelas Hashing

The hash()fungsi bekerja besar ketika Anda hash kelas atau contoh. Namun, berikut adalah satu masalah yang saya temukan dengan hash, mengenai objek:

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

Hash sama, bahkan setelah saya mengubah foo. Ini karena identitas foo tidak berubah, jadi hashnya sama. Jika Anda ingin hash berbeda tergantung pada definisi saat ini, solusinya adalah dengan memotong apa pun yang sebenarnya berubah. Dalam hal ini, __dict__atributnya:

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

Sayangnya, ketika Anda mencoba melakukan hal yang sama dengan kelas itu sendiri:

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

__dict__Properti kelas bukan kamus normal:

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

Berikut adalah mekanisme yang sama seperti sebelumnya yang akan menangani kelas dengan tepat:

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()))))

Anda dapat menggunakan ini untuk mengembalikan hash tuple dari banyak elemen yang Anda inginkan:

# -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__]))

CATATAN: semua kode di atas mengasumsikan Python 3.x. Tidak menguji di versi sebelumnya, meskipun saya berasumsi make_hash()akan bekerja, katakanlah, 2.7.2. Sejauh membuat contoh kerja, saya tidak tahu bahwa

func.__code__ 

harus diganti dengan

func.func_code
jomido
sumber
isinstance mengambil urutan untuk argumen kedua, sehingga isinstance (o, (set, tuple, list)) akan berfungsi.
Xealot
terima kasih telah membuat saya menyadari frozenset secara konsisten dapat hash querystring parameter :)
Xealot
1
Item perlu diurutkan untuk membuat hash yang sama jika urutan item dict berbeda tetapi nilai kunci tidak -> return hash (tuple (frozenset (diurutkan (new_o.items ()))))
Bas Koopmans
Bagus! Saya juga menambahkan panggilan ke hashsekitar daftar dan tupel. Kalau tidak, ini mengambil daftar bilangan bulat saya yang kebetulan menjadi nilai dalam kamus saya, dan mengembalikan daftar hash, yang bukan yang saya inginkan.
Osa
Frozenset adalah koleksi UNORDERED, jadi tidak ada untungnya dengan menyortir inputnya. Daftar dan tupel di sisi lain adalah koleksi ORDERED ("urutan"), dan nilai hash harus dipengaruhi oleh urutan item di dalamnya. Anda tidak harus menyortirnya!
RobM
14

Ini solusi yang lebih jelas.

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))  
smartnut007
sumber
Jika Anda mengubah if isinstance(o,list):ke if isinstance(obj, (set, tuple, list)):maka fungsi ini dapat bekerja pada objek apapun.
Peter Schorn
10

Kode di bawah ini menghindari penggunaan fungsi hash () Python karena tidak akan memberikan hash yang konsisten di me-restart Python (lihat fungsi hash di Python 3.3 mengembalikan hasil yang berbeda antara sesi ). make_hashable()akan mengkonversi objek ke tuple bersarang dan make_hash_sha256()juga akan mengkonversi repr()ke hash SHA256 yang disandikan 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=
Claudio Fahey
sumber
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))). Ini bukan solusi yang saya cari, tapi ini perantara yang bagus. Saya sedang berpikir untuk menambah type(o).__name__awal masing-masing tuple untuk memaksa diferensiasi.
Poik
Jika Anda ingin mengurutkan daftar juga:tuple(sorted((make_hashable(e) for e in o)))
Suraj
make_hash_sha256 () - bagus!
jtlz2
1
@ Suraj Anda seharusnya tidak ingin mengurutkan daftar sebelum hashing karena daftar yang memiliki isinya dalam urutan yang berbeda jelas bukan hal yang sama. Jika urutan item tidak masalah masalahnya adalah Anda menggunakan struktur data yang salah. Anda harus menggunakan set alih-alih daftar.
scottclowe
@scottclowe Itu sangat benar. Terima kasih telah menambahkan poin itu. Ada 2 skenario di mana Anda masih menginginkan daftar (tanpa kebutuhan pemesanan khusus) - 1. Daftar item berulang. 2. Saat Anda harus menggunakan JSON secara langsung. Karena JSON tidak mendukung representasi "set".
Suraj
5

Diperbarui dari 2013 balasan ...

Tidak ada jawaban di atas yang tampaknya dapat diandalkan bagi saya. Alasannya adalah penggunaan item (). Sejauh yang saya tahu, ini keluar dalam urutan yang tergantung pada mesin.

Bagaimana dengan ini?

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
Steve Yeago
sumber
Menurut Anda mengapa penting yang dict.itemstidak mengembalikan daftar yang dapat diperkirakan? frozensetmengurus itu
glarrain
2
Set, menurut definisi, tidak diurutkan. Dengan demikian urutan penambahan objek tidak relevan. Anda harus menyadari bahwa fungsi hashbawaan tidak peduli tentang bagaimana konten frozenset dicetak atau sesuatu seperti itu. Uji di beberapa mesin dan versi python dan Anda akan melihatnya.
glarrain
Mengapa Anda menggunakan hash ekstra () panggilan dalam nilai = hash ('% s ::% s'% (nilai, jenis (nilai))) ??
RuiDo
4

Untuk mempertahankan pesanan utama, alih-alih hash(str(dictionary))atau hash(json.dumps(dictionary))saya lebih suka solusi cepat dan kotor:

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

Ini akan bekerja bahkan untuk tipe suka DateTimedan banyak lagi yang bukan JSON serializable.

syirk3y
sumber
3
Siapa yang menjamin bahwa pformat atau json selalu menggunakan urutan yang sama?
ThiefMaster
1
@ThiefMaster, "Diubah dalam versi 2.5: Kamus diurutkan berdasarkan kunci sebelum tampilan dihitung; sebelum 2.5, kamus diurutkan hanya jika tampilan membutuhkan lebih dari satu baris, meskipun itu tidak didokumentasikan." ( Docs.python. org / 2 / library / pprint.html )
Arel
2
Ini sepertinya tidak berlaku bagi saya. Modul print dan pformat dipahami oleh penulis untuk tujuan tampilan dan bukan serialisasi. Karena itu, Anda tidak boleh merasa aman dengan berasumsi bahwa pformat akan selalu mengembalikan hasil yang terjadi pada pekerjaan.
David Sanders
3

Anda bisa menggunakan frozendictmodul pihak ketiga untuk membekukan dict Anda dan membuatnya menjadi hashable.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Untuk menangani objek bersarang, Anda bisa menggunakan:

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__))

Jika Anda ingin mendukung lebih banyak jenis, gunakan 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
Eric
sumber
Ini tidak bekerja, misalnya, untuk dictdari DataFrameobjek.
James Hirschorn
@ JamesHirschorn: Diperbarui gagal dengan keras
Eric
Lebih baik! Saya menambahkan elifklausa berikut untuk membuatnya bekerja dengan DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) Saya akan mengedit jawaban dan melihat apakah Anda menerimanya ...
James Hirschorn
1
BAIK. Saya melihat saya meminta sesuatu yang lebih dari "hashable", yang hanya menjamin bahwa objek yang sama akan memiliki hash yang sama. Saya sedang mengerjakan versi yang akan memberikan nilai yang sama antara berjalan, dan independen dari versi python, dll.
James Hirschorn
1
hashpengacakan adalah fitur keamanan yang disengaja diaktifkan secara default di python 3.7.
Eric
1

Anda dapat menggunakan perpustakaan peta untuk melakukan ini. Khususnya, maps.FrozenMap

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

Untuk menginstal maps, lakukan saja:

pip install maps

Ini menangani dictkasus bersarang juga:

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

Penafian: Saya adalah penulis mapsperpustakaan.

Pedro Cattori
sumber
Perpustakaan tidak mengurutkan daftar di dalam dikt. Dan karenanya ini dapat menghasilkan hash yang berbeda. Seharusnya ada opsi untuk menyortir daftar juga. Sebuah frozenset akan membantu, tapi saya ingin tahu bagaimana Anda menangani kasus ini dengan dict bersarang yang berisi daftar dicts. Karena dikt tidak dapat dihancurkan.
Suraj
1
@Suraj: itu tidak struktur bersarang menangani via .recurse. Lihat maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Memesan dalam daftar secara semantik bermakna, jika Anda menginginkan kemandirian pesanan, Anda dapat mengonversi daftar Anda menjadi set sebelum melakukan panggilan .recurse. Anda juga dapat menggunakan list_fnparameter untuk .recursemenggunakan struktur data hashable yang berbeda dari tuple(.eg frozenset)
Pedro Cattori
0

Salah satu cara untuk mendekati masalah adalah dengan membuat tuple dari item kamus:

hash(tuple(my_dict.items()))
Anonim
sumber
-8

Saya melakukannya seperti ini:

hash(str(my_dict))
garbanzio
sumber
1
Adakah yang bisa menjelaskan apa yang salah dengan metode ini?
mhristache
7
@maximi Kamus tidak stabil dalam hal keteraturan, jadi hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(sementara itu mungkin bekerja untuk beberapa kamus, kamus ini tidak dijamin berfungsi pada semua).
Vlad Frolov