Cách chính xác và tốt để thực hiện __hash __ () là gì?


150

Cách chính xác và tốt để thực hiện là __hash__()gì?

Tôi đang nói về chức năng trả về mã băm sau đó được sử dụng để chèn các đối tượng vào hashtables aka từ điển.

Khi __hash__()trả về một số nguyên và được sử dụng cho các đối tượng "tạo thùng" thành các hàm băm, tôi giả sử rằng các giá trị của số nguyên được trả về phải được phân phối đồng đều cho dữ liệu chung (để giảm thiểu va chạm). Một thực hành tốt để có được những giá trị như vậy là gì? Là va chạm một vấn đề? Trong trường hợp của tôi, tôi có một lớp nhỏ hoạt động như một lớp container chứa một số int, một số float và một chuỗi.

Câu trả lời:


185

Một cách dễ dàng, chính xác để thực hiện __hash__()là sử dụng một bộ khóa. Nó sẽ không nhanh như một hàm băm chuyên dụng, nhưng nếu bạn cần điều đó thì có lẽ bạn nên thực hiện kiểu trong C.

Đây là một ví dụ về việc sử dụng khóa cho hàm băm và đẳng thức:

class A:
    def __key(self):
        return (self.attr_a, self.attr_b, self.attr_c)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        if isinstance(other, A):
            return self.__key() == other.__key()
        return NotImplemented

Ngoài ra, tài liệu của__hash__ có nhiều thông tin hơn, có thể có giá trị trong một số trường hợp cụ thể.


1
Ngoài chi phí nhỏ từ việc bao thanh toán __keychức năng, đây là tốc độ nhanh nhất có thể. Chắc chắn, nếu các thuộc tính được biết là số nguyên và không có quá nhiều trong số chúng, tôi cho rằng bạn có khả năng có thể chạy nhanh hơn một chút với một số hàm băm cuộn tại nhà, nhưng có khả năng nó sẽ không được phân phối tốt. hash((self.attr_a, self.attr_b, self.attr_c))sẽ nhanh đến mức đáng ngạc nhiên (và chính xác ), vì việc tạo ra các tuples nhỏ được tối ưu hóa đặc biệt và nó đẩy công việc nhận và kết hợp các giá trị băm đến các nội trang C, thường nhanh hơn mã cấp Python.
ShadowRanger

Giả sử một đối tượng của lớp A đang được sử dụng làm khóa cho từ điển và nếu một thuộc tính của lớp A thay đổi, giá trị băm của nó cũng sẽ thay đổi. Điều đó sẽ không tạo ra một vấn đề?
Mr Matrix

1
Như câu trả lời của @ yêu.by.Jesus dưới đây đề cập, không nên xác định / ghi đè phương thức băm cho một đối tượng có thể thay đổi (được xác định theo mặc định và sử dụng id cho đẳng thức và so sánh).
Mr Matrix

@Miguel, tôi gặp vấn đề chính xác , điều gì xảy ra là từ điển trả về Nonemột khi khóa thay đổi. Cách tôi giải quyết nó là bằng cách lưu trữ id của đối tượng làm khóa thay vì chỉ đối tượng.
Jaswant P

@JaswantP Python theo mặc định sử dụng id của đối tượng làm khóa cho bất kỳ đối tượng có thể băm nào.
Mr Matrix

22

John Millikin đã đề xuất một giải pháp tương tự như thế này:

class A(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        return (isinstance(othr, type(self))
                and (self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))

    def __hash__(self):
        return hash((self._a, self._b, self._c))

Vấn đề với giải pháp này là hash(A(a, b, c)) == hash((a, b, c)). Nói cách khác, hàm băm va chạm với bộ dữ liệu của các thành viên chủ chốt. Có lẽ điều này không quan trọng lắm trong thực tế?

Cập nhật: các tài liệu Python hiện khuyên bạn nên sử dụng một tuple như trong ví dụ trên. Lưu ý rằng các tài liệu nêu

Thuộc tính bắt buộc duy nhất là các đối tượng so sánh bằng nhau có cùng giá trị băm

Lưu ý rằng điều ngược lại là không đúng sự thật. Các đối tượng không so sánh bằng nhau có thể có cùng giá trị băm. Một va chạm băm như vậy sẽ không làm cho một đối tượng thay thế một đối tượng khác khi được sử dụng làm khóa chính hoặc đặt phần tử miễn là các đối tượng không so sánh bằng nhau .

Giải pháp lỗi thời / xấu

Các tài liệu Python trên__hash__ gợi ý kết hợp băm của các tiểu hợp phần sử dụng một cái gì đó giống như XOR , mang đến cho chúng ta điều này:

class B(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        if isinstance(othr, type(self)):
            return ((self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))
        return NotImplemented

    def __hash__(self):
        return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
                hash((self._a, self._b, self._c)))

Cập nhật: như Blckknght chỉ ra, việc thay đổi thứ tự của a, b và c có thể gây ra vấn đề. Tôi đã thêm một bổ sung ^ hash((self._a, self._b, self._c))để nắm bắt thứ tự của các giá trị được băm. Cuối cùng này ^ hash(...)có thể được loại bỏ nếu các giá trị được kết hợp không thể được sắp xếp lại (ví dụ: nếu chúng có các loại khác nhau và do đó giá trị của _asẽ không bao giờ được gán cho _bhoặc _c, v.v.).


5
Bạn thường không muốn thực hiện XOR thẳng các thuộc tính với nhau, vì điều đó sẽ tạo ra xung đột nếu bạn thay đổi thứ tự của các giá trị. Nghĩa là, hash(A(1, 2, 3))sẽ bằng hash(A(3, 1, 2))(và họ sẽ cả băm bằng với bất kỳ khác AVí dụ với một hoán vị của 1, 23như các giá trị của nó). Nếu bạn muốn tránh trường hợp của bạn có cùng hàm băm như một tuple của các đối số của họ, chỉ cần tạo một giá trị sentinel (như một biến lớp hoặc là toàn cục), sau đó đưa nó vào tuple để được băm: return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght

1
Việc sử dụng của bạn isinstancecó thể có vấn đề, vì một đối tượng của một lớp con type(self)bây giờ có thể bằng với một đối tượng type(self). Vì vậy, bạn có thể thấy rằng việc thêm a Carvà a Fordvào a set()có thể chỉ dẫn đến một đối tượng được chèn, tùy thuộc vào thứ tự chèn. Ngoài ra, bạn có thể gặp phải tình huống a == bĐúng nhưng b == alà Sai.
MaratC

1
Nếu bạn đang phân lớp B, bạn có thể muốn thay đổi điều đó thànhisinstance(othr, B)
millerdev

7
Một suy nghĩ: bộ khóa có thể bao gồm loại lớp, điều này sẽ ngăn các lớp khác có cùng bộ thuộc tính khóa được hiển thị bằng nhau : hash((type(self), self._a, self._b, self._c)).
Ben Mosher

2
Bên cạnh quan điểm về việc sử dụng Bthay vì type(self), nó cũng thường được coi là thực hành tốt hơn để trở lại NotImplementedkhi gặp một loại không mong muốn __eq__thay vì False. Điều đó cho phép các loại do người dùng định nghĩa khác thực hiện một loại __eq__biết Bvà có thể so sánh bằng với nó, nếu họ muốn.
Đánh dấu Amery

16

Paul Larson của Microsoft Research đã nghiên cứu rất nhiều hàm băm. Anh ấy nói với tôi rằng

for c in some_string:
    hash = 101 * hash  +  ord(c)

làm việc tốt đáng ngạc nhiên cho một loạt các chuỗi. Tôi đã thấy rằng các kỹ thuật đa thức tương tự hoạt động tốt để tính toán một hàm băm của các trường con khác nhau.


8
Rõ ràng Java cũng làm theo cách tương tự nhưng sử dụng 31 thay vì 101
user229898

3
Lý do đằng sau việc sử dụng những con số này là gì? Có một lý do để chọn 101, hoặc 31?
bigblind

1
Dưới đây là lời giải thích cho các số nguyên tố: stackoverflow.com/questions/3613102/ ,. 101 dường như hoạt động đặc biệt tốt, dựa trên các thí nghiệm của Paul Larson.
George V. Reilly

4
Python sử dụng (hash * 1000003) XOR ord(c)cho các chuỗi với phép nhân 32 bit. [Trích dẫn ]
tylerl

4
Ngay cả khi điều này là đúng thì nó cũng không được sử dụng thực tế trong ngữ cảnh này vì các kiểu chuỗi Python dựng sẵn đã cung cấp một __hash__phương thức; chúng ta không cần phải tự lăn. Câu hỏi đặt ra là làm thế nào để triển khai __hash__cho một lớp do người dùng định nghĩa điển hình (với một loạt các thuộc tính trỏ đến các loại dựng sẵn hoặc có lẽ là các lớp do người dùng định nghĩa khác), mà câu trả lời này hoàn toàn không giải quyết.
Đánh dấu Amery

3

Tôi có thể cố gắng trả lời phần thứ hai của câu hỏi của bạn.

Các va chạm có thể sẽ không phải do chính mã băm mà từ ánh xạ mã băm đến một chỉ mục trong bộ sưu tập. Vì vậy, ví dụ hàm băm của bạn có thể trả về các giá trị ngẫu nhiên từ 1 đến 10000, nhưng nếu bảng băm của bạn chỉ có 32 mục, bạn sẽ bị va chạm khi chèn.

Ngoài ra, tôi sẽ nghĩ rằng các va chạm sẽ được giải quyết bằng bộ sưu tập trong nội bộ và có nhiều phương pháp để giải quyết va chạm. Đơn giản nhất (và tệ nhất) là, đưa ra một mục để chèn tại chỉ mục i, thêm 1 vào i cho đến khi bạn tìm thấy một điểm trống và chèn vào đó. Lấy lại sau đó hoạt động theo cách tương tự. Điều này dẫn đến việc truy xuất không hiệu quả đối với một số mục, vì bạn có thể có một mục yêu cầu đi qua toàn bộ bộ sưu tập để tìm!

Các phương pháp giải quyết va chạm khác làm giảm thời gian truy xuất bằng cách di chuyển các mục trong bảng băm khi một mục được chèn để trải rộng mọi thứ. Điều này làm tăng thời gian chèn nhưng giả sử bạn đọc nhiều hơn bạn chèn. Ngoài ra còn có các phương pháp thử và phân nhánh các mục va chạm khác nhau để các mục nhập phân cụm ở một vị trí cụ thể.

Ngoài ra, nếu bạn cần thay đổi kích thước bộ sưu tập, bạn sẽ cần phải làm lại mọi thứ hoặc sử dụng phương pháp băm động.

Nói tóm lại, tùy thuộc vào những gì bạn đang sử dụng mã băm cho bạn có thể phải thực hiện phương pháp giải quyết va chạm của riêng bạn. Nếu bạn không lưu trữ chúng trong bộ sưu tập, có lẽ bạn có thể thoát khỏi hàm băm chỉ tạo mã băm trong phạm vi rất lớn. Nếu vậy, bạn có thể chắc chắn rằng thùng chứa của bạn lớn hơn mức cần thiết (dĩ nhiên càng lớn càng tốt) tùy thuộc vào mối quan tâm bộ nhớ của bạn.

Dưới đây là một số liên kết nếu bạn quan tâm hơn:

băm kết hợp trên wikipedia

Wikipedia cũng có một bản tóm tắt các phương pháp giải quyết va chạm khác nhau:

Ngoài ra, " Tổ chức và xử lý tệp " của Tharp bao gồm rất nhiều phương pháp giải quyết va chạm. IMO nó là một tài liệu tham khảo tuyệt vời cho các thuật toán băm.


1

Một lời giải thích rất hay về thời điểm và cách thức thực hiện __hash__chức năng trên trang web của chương trình :

Chỉ cần một ảnh chụp màn hình để cung cấp tổng quan: (Truy cập 2019-12-13)

Ảnh chụp màn hình của https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13

Đối với việc triển khai phương pháp cá nhân, trang web được đề cập ở trên cung cấp một ví dụ phù hợp với câu trả lời của millerdev .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))

0

Phụ thuộc vào kích thước của giá trị băm bạn trả lại. Logic đơn giản là nếu bạn cần trả về int 32 bit dựa trên hàm băm của bốn int 32 bit, bạn sẽ bị va chạm.

Tôi sẽ ủng hộ hoạt động bit. Giống như, mã giả C sau đây:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

Một hệ thống như vậy cũng có thể hoạt động cho phao, nếu bạn chỉ đơn giản lấy chúng làm giá trị bit của chúng thay vì thực sự đại diện cho giá trị dấu phẩy động, có thể tốt hơn.

Đối với chuỗi, tôi có ít / không có ý tưởng.


Tôi biết rằng sẽ có va chạm. Nhưng tôi không biết làm thế nào những điều này được xử lý. Và hơn nữa, các giá trị thuộc tính của tôi kết hợp được phân bổ rất thưa thớt nên tôi đang tìm kiếm một giải pháp thông minh. Và bằng cách nào đó tôi mong đợi sẽ có một thực hành tốt nhất ở đâu đó.
dùng229898
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.