Băm một từ điển?


156

Đối với mục đích lưu trữ, tôi cần tạo khóa bộ đệm từ các đối số GET có trong một lệnh.

Hiện tại tôi đang sử dụng sha1(repr(sorted(my_dict.items())))( sha1()là một phương pháp tiện lợi sử dụng hashlib trong nội bộ) nhưng tôi tò mò liệu có cách nào tốt hơn không.


4
điều này có thể không hoạt động với dict lồng nhau. Giải pháp ngắn nhất là sử dụng json.dumps (my_dict, sort_keys = True) thay vào đó, sẽ tái diễn thành các giá trị dict.
Andrey Fedorov

2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 nói "Đầu ra từ dưa chua không được đảm bảo là hợp quy vì các lý do tương tự để ra lệnh và đặt thứ tự là không xác định. Đừng sử dụng dưa chua hoặc dấu vân tay hoặc repr để băm . "
Matthew Cornell

sắp xếp các khóa dict, không phải các mục, tôi cũng sẽ gửi các khóa cho hàm băm.
nyuwec

2
Câu chuyện thú vị về băm cấu trúc dữ liệu có thể thay đổi (như từ điển): python.org/dev/peps/pep-0351 được đề xuất cho phép các đối tượng đóng băng tùy ý, nhưng bị từ chối. Để biết lý do, hãy xem chủ đề này trong python-dev: mail.python.org/pipermail/python-dev/2006-F/2/060793.html
FluxLemur 29/03/18

Nếu dữ liệu của bạn là định dạng json và bạn muốn băm bất biến về mặt ngữ nghĩa, hãy kiểm tra github.com/schollii/sandals/blob/master/json_sem_hash.py . Nó hoạt động trên các cấu trúc lồng nhau (tất nhiên, kể từ json) và không phụ thuộc vào bên trong của dict như trật tự được bảo tồn (đã phát triển trong suốt vòng đời của python) và sẽ cho cùng một hàm băm nếu hai cấu trúc dữ liệu giống nhau về mặt ngữ nghĩa ( giống như về mặt {'a': 1, 'b':2}ngữ nghĩa giống như {'b':2, 'a':1}). Tôi chưa sử dụng nó cho bất cứ điều gì quá phức tạp, vì vậy YMMV nhưng phản hồi rất hoan nghênh.
Oliver

Câu trả lời:


110

Nếu từ điển của bạn không được lồng nhau, bạn có thể tạo một fro chụcet với các mục của dict và sử dụng hash():

hash(frozenset(my_dict.items()))

Điều này ít chuyên sâu hơn về mặt tính toán so với việc tạo chuỗi JSON hoặc biểu diễn của từ điển.

CẬP NHẬT: Vui lòng xem các ý kiến ​​dưới đây, tại sao phương pháp này có thể không tạo ra kết quả ổn định.


9
Điều này không làm việc cho tôi với một từ điển lồng nhau. Tôi chưa thử giải pháp dưới đây (quá phức tạp). Giải pháp của OP hoạt động hoàn toàn tốt. Tôi đã thay thế sha1 bằng hàm băm để lưu một lần nhập.
spatel

9
@Ceaser Điều đó sẽ không hoạt động vì tuple ngụ ý đặt hàng nhưng các mục chính tả không được sắp xếp. froundredet là tốt hơn.
Antimon

28
Cẩn thận với hàm băm tích hợp nếu có thứ gì đó cần nhất quán trên các máy khác nhau. Việc triển khai python trên các nền tảng đám mây như Heroku và GAE sẽ trả về các giá trị khác nhau cho hàm băm () trong các trường hợp khác nhau khiến cho mọi thứ phải được chia sẻ giữa hai hoặc nhiều "máy" (dynos trong trường hợp của heroku)
Ben Roberts

6
Điều thú vị là hash()chức năng này không tạo ra đầu ra ổn định. Điều này có nghĩa là, với cùng một đầu vào, nó trả về các kết quả khác nhau với các phiên bản khác nhau của cùng một trình thông dịch python. Đối với tôi, có vẻ như một số loại giá trị hạt giống được tạo ra mỗi khi trình thông dịch được bắt đầu.
Hermann Schachner

7
hy vọng. hạt giống được giới thiệu vì lý do bảo mật theo như tôi nhớ để thêm một số loại ngẫu nhiên bộ nhớ. Vì vậy, bạn không thể mong đợi hàm băm giống nhau giữa hai quá trình python
Nikokrock

137

Sử dụng sorted(d.items())không đủ để giúp chúng tôi có một repr ổn định. Một số giá trị trong đó dcũng có thể là từ điển và các khóa của chúng vẫn sẽ xuất hiện theo thứ tự tùy ý. Miễn là tất cả các khóa là chuỗi, tôi thích sử dụng:

json.dumps(d, sort_keys=True)

Điều đó nói rằng, nếu băm cần ổn định trên các máy khác nhau hoặc các phiên bản Python, tôi không chắc chắn rằng đây là khả năng chống đạn. Bạn có thể muốn thêm separatorsvà các ensure_asciiđối số để bảo vệ bản thân khỏi mọi thay đổi đối với các mặc định ở đó. Tôi đánh giá cao ý kiến.


6
Điều này chỉ là hoang tưởng, nhưng JSON cho phép hầu hết các ký tự hiển thị theo chuỗi mà không có bất kỳ thoát nào theo nghĩa đen, vì vậy bộ mã hóa phải đưa ra một số lựa chọn về việc có nên thoát các ký tự hay chỉ cần vượt qua chúng. Rủi ro sau đó là các phiên bản khác nhau (hoặc phiên bản tương lai) của bộ mã hóa có thể đưa ra các lựa chọn thoát khác nhau theo mặc định, và sau đó chương trình của bạn sẽ tính các giá trị băm khác nhau cho cùng một từ điển trong các môi trường khác nhau. Các ensure_asciitranh luận sẽ bảo vệ chống lại vấn đề hoàn toàn giả thuyết này.
Jack O'Connor

4
Tôi đã thử nghiệm hiệu suất của điều này với các tập dữ liệu khác nhau, nó nhanh hơn nhiều make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujson không đảm bảo thứ tự của các cặp dict, vì vậy không an toàn để làm điều đó
arthurprs

11
Giải pháp này chỉ hoạt động miễn là tất cả các khóa là chuỗi, ví dụ json.dumps ({'a': {(0, 5): 5, 1: 3}}) không thành công.
kadee

5
@LorenzoBelli, bạn có thể khắc phục điều đó bằng cách thêm default=strvào dumpslệnh. Có vẻ để làm việc độc đáo.
mlissner

63

EDIT : Nếu tất cả các khóa của bạn là chuỗi , thì trước khi tiếp tục đọc câu trả lời này, vui lòng xem giải pháp đơn giản hơn (và nhanh hơn) của Jack O'Connor (cũng hoạt động để băm từ điển lồng nhau).

Mặc dù câu trả lời đã được chấp nhận, tiêu đề của câu hỏi là "Băm một từ điển trăn" và câu trả lời không đầy đủ về tiêu đề đó. (Liên quan đến phần chính của câu hỏi, câu trả lời đã hoàn tất.)

Từ điển lồng nhau

Nếu một người tìm kiếm Stack Overflow để tìm cách băm từ điển, người ta có thể vấp phải câu hỏi có tiêu đề thích hợp này và không hài lòng nếu ai đó cố gắng băm nhiều từ điển lồng nhau. Câu trả lời ở trên sẽ không hoạt động trong trường hợp này và bạn sẽ phải thực hiện một số loại cơ chế đệ quy để truy xuất hàm băm.

Đây là một cơ chế như vậy:

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

Phần thưởng: Các đối tượng và lớp băm

Các hash()chức năng hoạt động tuyệt vời khi bạn băm lớp hoặc trường hợp. Tuy nhiên, đây là một vấn đề tôi tìm thấy với hàm băm, liên quan đến các đối tượng:

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

Băm là như nhau, ngay cả sau khi tôi đã thay đổi foo. Điều này là do danh tính của foo chưa thay đổi, vì vậy hàm băm là như nhau. Nếu bạn muốn foo băm khác nhau tùy thuộc vào định nghĩa hiện tại của nó, giải pháp là băm ra bất cứ điều gì thực sự thay đổi. Trong trường hợp này, __dict__thuộc tính:

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

Than ôi, khi bạn cố gắng làm điều tương tự với chính lớp đó:

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

__dict__Thuộc tính lớp không phải là một từ điển bình thường:

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

Đây là một cơ chế tương tự như trước đây sẽ xử lý các lớp một cách thích hợp:

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

Bạn có thể sử dụng điều này để trả về một bộ băm gồm nhiều yếu tố bạn muốn:

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

LƯU Ý: tất cả các mã trên giả định Python 3.x. Không thử nghiệm trong các phiên bản trước, mặc dù tôi giả sử make_hash()sẽ hoạt động trong 2.7.2. Theo như làm công việc ví dụ, tôi làm biết rằng

func.__code__ 

nên được thay thế bằng

func.func_code

isinstance lấy một chuỗi cho đối số thứ hai, do đó isinstance (o, (set, tuple, list)) sẽ hoạt động.
Xealot

cảm ơn vì đã khiến tôi nhận ra froundredet có thể băm liên tục các tham số chuỗi truy vấn :)
Xealot

1
Các mục cần được sắp xếp để tạo cùng một hàm băm nếu thứ tự mục chính tả khác nhau nhưng các giá trị khóa không -> trả về hàm băm (tuple (froundredet (được sắp xếp (new_o.items ()))))
Bas Koopmans

Đẹp! Tôi cũng đã thêm một cuộc gọi vào hashdanh sách xung quanh và bộ dữ liệu. Mặt khác, nó lấy danh sách các số nguyên của tôi là giá trị trong từ điển của tôi và trả về danh sách băm, đó không phải là điều tôi muốn.
osa

Một froundredet là một bộ sưu tập UNORDERED, vì vậy không có gì để đạt được bằng cách sắp xếp các đầu vào của nó. Mặt khác, danh sách và bộ dữ liệu là các bộ sưu tập ORDERED ("chuỗi"), và do đó, giá trị băm sẽ bị ảnh hưởng bởi thứ tự của các mục trong đó. Bạn không nên sắp xếp chúng!
RobM

14

Đây là một giải pháp rõ ràng hơn.

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

Nếu bạn thay đổi if isinstance(o,list):để if isinstance(obj, (set, tuple, list)):sau đó chức năng này có thể làm việc trên bất kỳ đối tượng.
Peter Schorn

10

Mã bên dưới tránh sử dụng hàm băm Python () vì nó sẽ không cung cấp các giá trị băm nhất quán trong quá trình khởi động lại Python (xem hàm băm trong Python 3.3 trả về các kết quả khác nhau giữa các phiên ). make_hashable()sẽ chuyển đổi đối tượng thành các bộ dữ liệu lồng nhau và make_hash_sha256()cũng sẽ chuyển đổi repr()hàm băm SHA256 được mã hóa 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))). Đây không phải là giải pháp tôi đang tìm kiếm, nhưng nó là một trung gian tốt. Tôi đang nghĩ đến việc thêm type(o).__name__vào đầu mỗi bộ dữ liệu để buộc phân biệt.
Poik

Nếu bạn cũng muốn sắp xếp danh sách:tuple(sorted((make_hashable(e) for e in o)))
Suraj

make_hash_sha256 () - tốt đẹp!
jtlz2

1
@Suraj Bạn không nên sắp xếp danh sách trước khi băm vì các danh sách có nội dung theo thứ tự khác nhau chắc chắn không giống nhau. Nếu thứ tự của các mục không quan trọng, vấn đề là bạn đang sử dụng cấu trúc dữ liệu sai. Bạn nên sử dụng một bộ thay vì một danh sách.
scottclowe

@scottclowe Điều đó rất đúng. Cảm ơn đã thêm điểm đó. Có 2 kịch bản mà bạn vẫn muốn có một danh sách (không có nhu cầu đặt hàng cụ thể) - 1. Danh sách các mục lặp lại. 2. Khi bạn sử dụng JSON trực tiếp. Vì JSON không hỗ trợ biểu diễn "set".
Suraj

5

Cập nhật từ năm 2013 trả lời ...

Không có câu trả lời nào ở trên có vẻ đáng tin cậy đối với tôi. Lý do là việc sử dụng các mặt hàng (). Theo như tôi biết, điều này xuất hiện theo thứ tự phụ thuộc vào máy.

Làm thế nào về điều này thay thế?

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

Tại sao bạn nghĩ rằng vấn đề dict.itemskhông trả về một danh sách dự đoán theo thứ tự? frozensetchăm sóc điều đó
glarrain

2
Một bộ, theo định nghĩa, là không có thứ tự. Do đó thứ tự mà các đối tượng được thêm vào là không liên quan. Bạn phải nhận ra rằng chức năng tích hợp hashkhông quan tâm đến cách các nội dung hàng chục được in hoặc một cái gì đó tương tự. Kiểm tra nó trong một số máy và phiên bản python và bạn sẽ thấy.
glarrain

Tại sao bạn sử dụng lệnh gọi hàm băm () trong value = hash ('% s ::% s'% (value, type (value))) ??
RuiDo

4

Để duy trì trật tự quan trọng, thay vì hash(str(dictionary))hoặc hash(json.dumps(dictionary))tôi thích giải pháp nhanh và bẩn:

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

Nó sẽ hoạt động ngay cả đối với các kiểu như DateTimevà nhiều hơn nữa không phải là tuần tự hóa JSON.


3
Ai đảm bảo rằng pformat hoặc json luôn sử dụng cùng một thứ tự?
ThiefMaster

1
@ThiefMaster, "Đã thay đổi trong phiên bản 2.5: Từ điển được sắp xếp theo khóa trước khi màn hình được tính toán, trước 2.5, một từ điển chỉ được sắp xếp nếu màn hình của nó yêu cầu nhiều hơn một dòng, mặc dù điều đó không được ghi lại." ( Docs.python. org / 2 / library / pprint.html )
Arel

2
Điều này dường như không hợp lệ với tôi. Các mô-đun pprint và pformat được các tác giả hiểu là nhằm mục đích hiển thị và không phải là tuần tự hóa. Vì điều này, bạn không nên cảm thấy an toàn khi cho rằng pformat sẽ luôn trả về kết quả xảy ra với công việc.
David Sanders

3

Bạn có thể sử dụng frozendictmô-đun của bên thứ ba để đóng băng chính tả của mình và làm cho nó có thể băm được.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Để xử lý các đối tượng lồng nhau, bạn có thể đi với:

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

Nếu bạn muốn hỗ trợ nhiều loại hơn, hãy sử dụng 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

Điều này không làm việc, ví dụ, đối với một dictsố DataFrameđối tượng.
James Hirschorn

@JamesHirschorn: Cập nhật thất bại lớn
Eric

Tốt hơn! Tôi đã thêm elifmệnh đề sau để làm cho nó hoạt động với DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) Tôi sẽ chỉnh sửa câu trả lời và xem bạn có chấp nhận nó không ...
James Hirschorn 15/03/19

1
ĐỒNG Ý. Tôi thấy tôi đã yêu cầu một cái gì đó nhiều hơn "có thể băm", điều này chỉ đảm bảo rằng các đối tượng bằng nhau sẽ có cùng hàm băm. Tôi đang làm việc trên một phiên bản sẽ cho cùng một giá trị giữa các lần chạy và độc lập với phiên bản python, v.v.
James Hirschorn

1
hashtính ngẫu nhiên là tính năng bảo mật có chủ ý được bật theo mặc định trong python 3.7.
Eric

1

Bạn có thể sử dụng thư viện bản đồ để làm điều này. Cụ thể, maps.F FrozenMap

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

Để cài đặt maps, chỉ cần làm:

pip install maps

Nó cũng xử lý dicttrường hợp lồng nhau :

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

Tuyên bố miễn trừ trách nhiệm: Tôi là tác giả của mapsthư viện.


Thư viện không sắp xếp danh sách bên trong một lệnh. Và do đó điều này có thể tạo ra băm khác nhau. Nên có một tùy chọn để sắp xếp một danh sách quá. Một froundredet sẽ giúp đỡ, nhưng tôi tự hỏi làm thế nào bạn sẽ xử lý vụ việc với một lệnh đọc lồng nhau có chứa một danh sách các ký tự. Như dict là không thể.
Suraj

1
@Suraj: nó không xử lý cấu trúc lồng nhau qua .recurse. Xem maps.readthedocs.io/en/latest/api.html#maps.F FrozenMap.recurse . Đặt hàng trong danh sách có ý nghĩa về mặt ngữ nghĩa, nếu bạn muốn độc lập đơn hàng, bạn có thể chuyển đổi danh sách của mình thành các bộ trước khi gọi .recurse. Bạn cũng có thể sử dụng list_fntham số để .recursesử dụng cấu trúc dữ liệu có thể tuplebăm khác với (.eg frozenset)
Pedro Cattori

0

Một cách để tiếp cận vấn đề là tạo ra một bộ các mục của từ điển:

hash(tuple(my_dict.items()))

-8

Tôi làm như thế này:

hash(str(my_dict))

1
Ai đó có thể giải thích những gì là sai với phương pháp này?
mhristache

7
@maximi Từ điển không ổn định về mặt trật tự, do đó hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(trong khi nó có thể hoạt động đối với một số từ điển, nó không được đảm bảo để hoạt động trên tất cả).
Vlad Frolov
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.