2012-04-20 30 views
23

docs nói rằng một lớp có thể băm miễn là nó xác định phương pháp __hash__ và phương pháp __eq__. Tuy nhiên:Điều gì làm cho lớp do người dùng định nghĩa không thể thực hiện được?

class X(list): 
    # read-only interface of `tuple` and `list` should be the same, so reuse tuple.__hash__ 
    __hash__ = tuple.__hash__ 

x1 = X() 
s = {x1} # TypeError: unhashable type: 'X' 

Điều gì làm cho X không thể hoạt động?

Lưu ý rằng tôi phải có danh sách giống hệt nhau (về mặt bình đẳng) để được băm với cùng một giá trị; nếu không, tôi sẽ violate this requirement trên hàm băm:

Thuộc tính yêu cầu duy nhất là đối tượng mà so sánh tương đương có giá trị băm cùng

Các tài liệu làm cảnh báo rằng một đối tượng hashable không nên được sửa đổi trong suốt cuộc đời của nó, và tất nhiên tôi không sửa đổi các trường hợp của X sau khi tạo. Tất nhiên, thông dịch viên sẽ không kiểm tra điều đó.

+2

Yeah, các giao diện chỉ đọc là như nhau, nhưng tại sao bạn mong đợi tuple .__ hash__ để chỉ sử dụng các giao diện bên ngoài của riêng của nó lớp học? Đặc biệt khi viết bằng C. Sử dụng giao diện bên ngoài sẽ chậm hơn nhiều. Bạn không thể mong đợi một cách hợp lý một phương thức từ lớp A để làm việc cho lớp B trừ khi lớp B được phân lớp từ lớp A. Bạn thậm chí có cố gắng gọi x1 .__ hash __() xem liệu nó có hoạt động không? –

+0

@LennartRegebro Có, tôi đồng ý ... Xem nhận xét cuối cùng của tôi cho http: //stackoverflow.com/a/10254636/336527 ... Tôi vừa bị đóng băng não. – max

Trả lời

15

Chỉ cần đặt phương thức __hash__ thành phương thức của lớp tuple là không đủ. Bạn đã không thực sự nói với nó làm thế nào để băm khác nhau. tuples có thể băm vì chúng không thay đổi được. Nếu bạn thực sự muốn làm cho bạn làm ví dụ cụ thể, có thể là như sau:

class X2(list): 
    def __hash__(self): 
     return hash(tuple(self)) 

Trong trường hợp này, bạn thực sự xác định cách băm lớp con danh sách tùy chỉnh của mình. Bạn chỉ cần xác định chính xác cách nó có thể tạo ra một băm. Bạn có thể băm vào bất cứ điều gì bạn muốn, như trái ngược với sử dụng phương pháp băm của tuple:

def __hash__(self): 
    return hash("foobar"*len(self)) 
+0

Nhưng không phải là 'tuple .__ hash__' một hàm lấy một bộ dữ liệu và trả về một số? Làm thế nào mà chức năng "thông báo" rằng đối tượng của tôi thực sự là một 'danh sách' chứ không phải là một' tuple' - đọc API cho hai loại là giống hệt nhau. – max

+0

@max: 'tuple .__ hash__' là phương thức ràng buộc của lớp tuple. Bạn không thay đổi bất cứ điều gì thực hiện của nó đang làm bên trong phương pháp đó để băm. Xác định của riêng bạn. – jdi

+0

'băm ((1,2,3))' là giống như '(1,2,3) .__ hash__'; đó là giống như 'tuple .__ hash __ ((1,2,3))', phải không? Vậy 'tuple .__ hash__' dựa vào API không công khai của lớp' tuple', và vì vậy nó phá vỡ thông báo lỗi khó hiểu khi truyền một thể hiện của một lớp khác khớp với API công khai 'tuple'? Tôi cho rằng nó giải thích nó .. nhưng một chút bất ngờ.' – max

3

Nếu bạn không sửa đổi các trường hợp X sau khi tạo, tại sao không phải là bạn subclassing tuple?

Nhưng tôi sẽ chỉ ra rằng điều này thực sự không gây ra lỗi, ít nhất là trong Python 2.6.

>>> class X(list): 
...  __hash__ = tuple.__hash__ 
...  __eq__ = tuple.__eq__ 
... 
>>> x = X() 
>>> s = set((x,)) 
>>> s 
set([[]]) 

Tôi ngần ngại nói "hoạt động" vì điều này không làm những gì bạn nghĩ.

>>> a = X() 
>>> b = X((5,)) 
>>> hash(a) 
4299954584 
>>> hash(b) 
4299954672 
>>> id(a) 
4299954584 
>>> id(b) 
4299954672 

Chỉ sử dụng id đối tượng làm băm. Khi bạn thực sự gọi __hash__ bạn vẫn gặp lỗi; tương tự cho __eq__.

>>> a.__hash__() 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: descriptor '__hash__' for 'tuple' objects doesn't apply to 'X' object 
>>> X().__eq__(X()) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: descriptor '__eq__' for 'tuple' objects doesn't apply to 'X' object 

Tôi tập hợp mà các internals python, đối với một số lý do, được phát hiện X__hash__ và một phương pháp __eq__, nhưng không gọi họ.

Đạo đức của tất cả điều này là: chỉ cần viết hàm băm thực. Vì đây là một đối tượng chuỗi, chuyển đổi nó thành một tuple và băm là cách tiếp cận rõ ràng nhất.

def __hash__(self): 
    return hash(tuple(self)) 
+0

Tôi rất tiếc, câu hỏi này được lấy ra khỏi ngữ cảnh của một câu hỏi khác. Tôi chỉ bối rối về hành vi đặc biệt này. Lý do tôi danh sách phân lớp là một chút phức tạp (xem thảo luận trong ý kiến ​​để [câu hỏi này] (http://stackoverflow.com/questions/10253783/making-a-list-subclass-hashable)). – max

+0

Mã không hoạt động đối với tôi trong ActiveState Python 3.2. Có lẽ hành vi đã thay đổi gần đây? – max

+0

Tôi đang sử dụng Python 2.6. Trong mọi trường hợp, bạn không muốn hành vi này, bởi vì sử dụng 'id' như là chìa khóa không thực sự là một ý tưởng tốt. Tốt hơn là chỉ cần chuyển đổi thành tuple và băm đó. Và thực sự - tôi xin lỗi; đây chỉ là một cách tiếp cận khá rắc rối cho vấn đề đối với tôi. – senderle

5

gì bạn có thể và nên làm, dựa trên câu hỏi khác của bạn, là: không phân lớp bất cứ điều gì, chỉ cần gói gọn một tuple. Nó hoàn toàn tốt để làm như vậy trong init.

class X(object): 
    def __init__(self, *args): 
     self.tpl = args 
    def __hash__(self): 
     return hash(self.tpl) 
    def __eq__(self, other): 
     return self.tpl == other 
    def __repr__(self): 
     return repr(self.tpl) 

x1 = X() 
s = {x1} 

trong đó sản lượng:

>>> s 
set([()]) 
>>> x1 
() 
+0

Bạn nói đúng, đối với nhiều trường hợp sử dụng, đây là giải pháp đơn giản nhất, sạch nhất; +1 – senderle

4

Từ các tài liệu Python3:

Nếu một lớp không định nghĩa một phương pháp __eq __() nó không nên xác định một hoạt động __hash __() hoặc ; nếu nó định nghĩa __eq __() nhưng không __hash __(), các cá thể của nó sẽ không thể sử dụng được dưới dạng các mục trong các bộ sưu tập có thể băm. Nếu một lớp xác định các đối tượng có thể thay đổi và thực hiện phương thức __eq __(), nó sẽ không triển khai __hash __(), vì việc triển khai các bộ sưu tập có thể băm yêu cầu giá trị băm của một khóa là là không thay đổi (nếu giá trị băm của đối tượng thay đổi. nhóm băm sai).

Ref: object.__hash__(self)

Mẫu mã:

class Hashable: 
    pass 

class Unhashable: 
    def __eq__(self, other): 
     return (self == other) 

class HashableAgain: 
    def __eq__(self, other): 
     return (self == other) 

    def __hash__(self): 
     return id(self) 

def main(): 
    # OK 
    print(hash(Hashable())) 
    # Throws: TypeError("unhashable type: 'X'",) 
    print(hash(Unhashable())) 
    # OK 
    print(hash(HashableAgain())) 
+0

Có '__hash__' cần phải là duy nhất không? Giả sử bạn muốn các trường hợp của 'HashableAgain' được so sánh dựa trên các tiêu chí bạn đã xác định trong' __eq__', bạn có thể trả về một hằng số nguyên trong '__hash__' không? (Tôi không thực sự hiểu làm thế nào băm) được sử dụng trong việc quyết định thành viên của một đối tượng trong một tập hợp. –

+0

@MinhTran: Nói chung, băm không phải là duy nhất, nhưng _relatively_ duy nhất. Nó được sử dụng để xô các giá trị trong một bản đồ. Nếu bạn sử dụng giá trị không đổi cho hàm băm, tất cả các giá trị sẽ xuất hiện trong cùng một nhóm, do đó hiệu suất sẽ rất khủng khiếp ... nhưng nó vẫn hoạt động! – kevinarpe

Các vấn đề liên quan