Để hiểu điều gì đang diễn ra, bạn phải hiểu cách nhà điều hành in
, membership test hoạt động với các loại khác nhau.
Đối với danh sách, điều này khá đơn giản do danh sách cơ bản là: Các mảng được sắp xếp không quan tâm đến các bản sao. Cách duy nhất có thể để tạo thành một thử nghiệm thành viên ở đây là để lặp qua danh sách và kiểm tra mọi mục trên bình đẳng. Một cái gì đó như thế này:
# x in lst
for item in lst:
if x == item:
return True
return False
Từ điển có một chút khác biệt: Chúng là các bảng băm là khóa có nghĩa là duy nhất. Bảng băm yêu cầu các khóa phải là hashable, về cơ bản có nghĩa là cần phải có một hàm rõ ràng để chuyển đổi đối tượng thành một số nguyên. Giá trị băm này sau đó được sử dụng để đặt ánh xạ khóa/giá trị vào đâu đó trong bảng băm.
Vì giá trị băm xác định vị trí trong bảng băm mà một mục được đặt, điều quan trọng là các đối tượng được tạo giống hệt nhau sẽ tạo ra giá trị băm giống nhau. Vì vậy, hàm ý sau đây phải đúng: x == y => hash(x) == hash(y)
. Ngược lại không cần phải đúng; hoàn toàn hợp lệ để có các đối tượng khác nhau tạo ra cùng một giá trị băm.
Khi kiểm tra thành viên trên từ điển được thực hiện, từ điển trước tiên sẽ tìm giá trị băm. Nếu nó có thể tìm thấy nó, sau đó nó sẽ thực hiện một kiểm tra bình đẳng trên tất cả các mục nó tìm thấy; nếu nó không tìm thấy giá trị băm, sau đó nó giả định rằng đó là một đối tượng khác nhau:
# x in dct
h = hash(x)
items = getItemsForHash(dct, h)
for item in items:
if x == item:
return True
# items is empty, or no match inside the loop
return False
Kể từ khi bạn nhận được kết quả mong muốn khi sử dụng một thử nghiệm thành viên chống lại một danh sách, đó có nghĩa là đối tượng của bạn thực hiện việc so sánh bình đẳng (__eq__
) chính xác. Nhưng kể từ khi bạn không nhận được kết quả chính xác khi sử dụng một từ điển, có vẻ là một việc thực hiện __hash__
đó là không đồng bộ với việc thực hiện so sánh bình đẳng:
>>> class SomeType:
def __init__ (self, x):
self.x = x
def __eq__ (self, other):
return self.x == other.x
def __hash__ (self):
# bad hash implementation
return hash(id(self))
>>> l = [SomeType(1)]
>>> d = { SomeType(1): 'x' }
>>> x = SomeType(1)
>>> x in l
True
>>> x in d
False
Lưu ý rằng đối với các lớp học kiểu mới bằng Python 2 (các lớp kế thừa từ object
), "triển khai băm xấu" (dựa trên id đối tượng) là mặc định. Vì vậy, khi bạn không thực hiện chức năng của riêng bạn __hash__
, nó vẫn sử dụng chức năng đó.Điều này cuối cùng có nghĩa là trừ khi __eq__
của bạn chỉ thực hiện kiểm tra nhận dạng (mặc định), hàm băm sẽ không đồng bộ.
Vì vậy, giải pháp là triển khai __hash__
theo cách nó phù hợp với các quy tắc được sử dụng trong __eq__
. Ví dụ: nếu bạn so sánh hai thành viên self.x
và self.y
, thì bạn nên sử dụng hàm băm phức hợp trên hai thành viên đó. Cách dễ nhất để làm điều đó là để trả về giá trị hash của một tuple của những giá trị:
class SomeType (object):
def __init__ (self, x, y):
self.x = x
self.y = y
def __eq__ (self, other):
return self.x == other.x and self.y == other.y
def __hash__ (self):
return hash((self.x, self.y))
Lưu ý rằng bạn không nên thực hiện một hashable đối tượng nếu nó là có thể thay đổi:
Nếu một định nghĩa lớp đối tượng có thể thay đổi và triển khai phương thức __eq__()
, không nên triển khai __hash__()
do việc triển khai bộ sưu tập có thể băm yêu cầu giá trị băm của khóa không thay đổi (nếu giá trị băm của đối tượng thay đổi, nó sẽ nằm trong nhóm băm sai).
@vaultah Nó phải (nếu không bạn sẽ nhận được một TypeError unhashable), nhưng việc thực hiện có thể sẽ không phù hợp với việc thực hiện của '__eq__'. – poke
Bạn đã triển khai 'to_user' và lớp chính như thế nào? Từ điển Python không bảo toàn các đối tượng trùng lặp vì bạn có cùng giá trị '__hash__', nhưng nếu bạn tạo nhiều cá thể từ một lớp mỗi lần bạn sẽ nhận được một đối tượng mới với giá trị băm khác nhau. (với do thời điểm này mà họ có cùng một đại diện), nhưng kết quả trong từ điển của bạn sẽ không được đại diện bởi vì họ là cùng một chuỗi và do đó có giá trị băm giống nhau. – Kasramvd
@poke Bạn đã đăng câu trả lời tuyệt vời bên dưới +1. Tuy nhiên, nhận xét của bạn về TypeError không thể sửa được là không chính xác, [như được hiển thị trong câu trả lời này] (http://stackoverflow.com/a/17445665/1431750). Mã số – aneroid