Phân đoạn có thể băm trong Python


91

Như một bài tập và chủ yếu là để giải trí cho riêng tôi, tôi đang triển khai trình phân tích cú pháp packrat backtracking. Nguồn cảm hứng cho điều này là tôi muốn có ý tưởng tốt hơn về cách các macro hygenic sẽ hoạt động trong một ngôn ngữ giống algol (như được áp dụng cho các phương ngữ ngọng tự do cú pháp mà bạn thường thấy). Do đó, các lần chuyển khác nhau qua đầu vào có thể thấy các ngữ pháp khác nhau, do đó, kết quả phân tích cú pháp được lưu trong bộ nhớ cache là không hợp lệ, trừ khi tôi cũng lưu trữ phiên bản hiện tại của ngữ pháp cùng với kết quả phân tích cú pháp được lưu trong bộ nhớ cache. ( CHỈNH SỬA : hệ quả của việc sử dụng bộ sưu tập khóa-giá trị này là chúng phải là bất biến, nhưng tôi không có ý định để lộ giao diện cho phép chúng được thay đổi, vì vậy bộ sưu tập có thể thay đổi hoặc bất biến đều được)

Vấn đề là các hệ số python không thể xuất hiện dưới dạng chìa khóa cho các hệ thống khác. Ngay cả việc sử dụng một tuple (như tôi vẫn đang làm) cũng không giúp được gì.

>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> 

Tôi đoán nó phải được giảm xuống. Giờ đây, thư viện tiêu chuẩn của python cung cấp gần đúng những gì tôi cần, collections.namedtuplecó cú pháp rất khác, nhưng có thể được sử dụng làm khóa. tiếp tục từ phiên trên:

>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}

Đồng ý. Nhưng tôi phải tạo một lớp cho mỗi tổ hợp phím có thể có trong quy tắc mà tôi muốn sử dụng, điều này không quá tệ, bởi vì mỗi quy tắc phân tích cú pháp biết chính xác tham số mà nó sử dụng, vì vậy lớp đó có thể được xác định cùng một lúc. dưới dạng hàm phân tích quy tắc.

Chỉnh sửa: Một vấn đề bổ sung với namedtuples là chúng đúng vị trí. Hai bộ giá trị trông giống như chúng phải khác nhau trên thực tế có thể giống nhau:

>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False

tl'dr: Làm cách nào để lấy dicts có thể được sử dụng làm chìa khóa chodict s ?

Sau một chút về câu trả lời, đây là giải pháp hoàn chỉnh hơn mà tôi đang sử dụng. Lưu ý rằng điều này có tác dụng thêm một chút để làm cho các kết quả không thay đổi được cho các mục đích thực tế. Tất nhiên vẫn khá dễ dàng để hack xung quanh nó bằng cách gọi điện dict.__setitem__(instance, key, value)nhưng chúng tôi đều là người lớn ở đây.

class hashdict(dict):
    """
    hashable dict implementation, suitable for use as a key into
    other dicts.

        >>> h1 = hashdict({"apples": 1, "bananas":2})
        >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
        >>> h1+h2
        hashdict(apples=1, bananas=3, mangoes=5)
        >>> d1 = {}
        >>> d1[h1] = "salad"
        >>> d1[h1]
        'salad'
        >>> d1[h2]
        Traceback (most recent call last):
        ...
        KeyError: hashdict(bananas=3, mangoes=5)

    based on answers from
       http://stackoverflow.com/questions/1151658/python-hashable-dicts

    """
    def __key(self):
        return tuple(sorted(self.items()))
    def __repr__(self):
        return "{0}({1})".format(self.__class__.__name__,
            ", ".join("{0}={1}".format(
                    str(i[0]),repr(i[1])) for i in self.__key()))

    def __hash__(self):
        return hash(self.__key())
    def __setitem__(self, key, value):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def __delitem__(self, key):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def clear(self):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def pop(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def popitem(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def setdefault(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def update(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    # update is not ok because it mutates the object
    # __add__ is ok because it creates a new object
    # while the new object is under construction, it's ok to mutate it
    def __add__(self, right):
        result = hashdict(self)
        dict.update(result, right)
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Các hashdictphải bất biến, ít nhất là sau khi bạn bắt đầu băm nó, vậy tại sao không bộ nhớ cache keyhashgiá trị như các thuộc tính của hashdictđối tượng? Tôi đã sửa đổi __key()__hash__()thử nghiệm để xác nhận rằng nó nhanh hơn nhiều. SO không cho phép mã được định dạng trong nhận xét, vì vậy tôi sẽ liên kết nó ở đây: sam.aiki.info/hashdict.py
Sam Watkins

Câu trả lời:


67

Đây là cách dễ dàng để tạo một từ điển có thể băm. Chỉ cần nhớ không thay đổi chúng sau khi nhúng vào từ điển khác vì những lý do rõ ràng.

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))

7
Điều này không đảm bảo rõ ràng tính nhất quán của eqbăm trong khi câu trả lời trước đó của tôi thực hiện thông qua việc sử dụng phương thức __key (trong thực tế, cả hai phương pháp đều hoạt động, mặc dù phương pháp này có thể bị chậm lại bằng cách tạo danh sách ngay lập tức không cần thiết - có thể sửa bằng s / items / iteritems / - giả sử Python 2. * như bạn không nói ;-).
Alex Martelli,

5
Có lẽ sẽ tốt hơn nếu chỉ sử dụng một bộ định giá hơn là một bộ mã có phân loại. Điều này không chỉ sẽ nhanh hơn mà bạn không thể cho rằng các khóa từ điển có thể so sánh được.
asmeurer

1
Dường như có phải là một cách để tránh một hàm băm đó là O(n*log(n))nơi nlà số dictmục. Có ai biết liệu frozensethàm băm của Python có chạy trong thời gian tuyến tính không?
Tom Karzes

2
@HelloGoodbye Một dict cũng có thể được tạo như thế này dict(key1=value1, key2=value2,...)hoặc thế này dict([(key1, value1), (key2, value2),...)]). Điều này cũng áp dụng tương tự. Việc tạo ra bạn được đăng được gọi là đen
smido

2
@smido: Cảm ơn. Tôi cũng thấy rằng bạn chỉ có thể ép kiểu, nghĩa là hashabledict({key_a: val_a, key_b: val_b, ...}).
HelloGoodbye

62

Hashables phải là bất biến - không thực thi điều này nhưng TIN TƯỞNG bạn không thay đổi một mệnh lệnh sau lần đầu tiên sử dụng nó làm khóa, cách tiếp cận sau sẽ hoạt động:

class hashabledict(dict):
  def __key(self):
    return tuple((k,self[k]) for k in sorted(self))
  def __hash__(self):
    return hash(self.__key())
  def __eq__(self, other):
    return self.__key() == other.__key()

Nếu bạn cần phải thay đổi các con số của mình và VẪN muốn sử dụng chúng làm chìa khóa, độ phức tạp sẽ bùng nổ hàng trăm lần - không có nghĩa là không thể thực hiện được, nhưng tôi sẽ đợi cho đến khi có một chỉ dẫn RẤT cụ thể trước khi tôi đi vào cuộc chiến đáng kinh ngạc đó! -)


Tôi chắc chắn không muốn thay đổi cục diện khi chúng đã được chuẩn bị. Điều đó sẽ làm cho phần còn lại của thuật toán packrad sụp đổ.
SingleNegationElimination

Sau đó, lớp con mà tôi đề xuất sẽ hoạt động - lưu ý cách nó bỏ qua vấn đề "vị trí" ( trước khi bạn chỉnh sửa câu hỏi của mình để chỉ ra nó ;-) với sortedtrong __key ;-).
Alex Martelli,

Hành vi phụ thuộc vào vị trí của têntuple làm tôi ngạc nhiên. Tôi đã chơi với nó, nghĩ nó vẫn có thể là một cách dễ dàng hơn để giải quyết vấn đề, nhưng điều đó khá nhiều tiêu tan mọi hy vọng của tôi (và sẽ đòi hỏi một rollback :()
SingleNegationElimination

Giả sử tôi có một câu chính tả và tôi muốn chuyển nó thành một hashabledict. Làm thế nào tôi sẽ làm điều đó?
jononomo

@JonCrowell xem những câu hỏi này để biết ý tưởng và làm rõ: stackoverflow.com/questions/3464061/… , stackoverflow.com/questions/9112300/… , stackoverflow.com/questions/18020074/…
tối đa

32

Tất cả những gì cần thiết để làm cho từ điển có thể sử dụng được cho mục đích của bạn là thêm phương thức __hash__:

class Hashabledict(dict):
    def __hash__(self):
        return hash(frozenset(self))

Lưu ý, việc chuyển đổi kiểu chữ sẽ hoạt động cho tất cả các từ điển (nghĩa là nó không yêu cầu các phím phải có thể sắp xếp). Tương tự như vậy, không có giới hạn đối với các giá trị từ điển.

Nếu có nhiều từ điển có các khóa giống hệt nhau nhưng có các giá trị khác nhau, thì cần phải băm tính đến các giá trị. Cách nhanh nhất để làm điều đó là:

class Hashabledict(dict):
    def __hash__(self):
        return hash((frozenset(self), frozenset(self.itervalues())))

Điều này nhanh hơn frozenset(self.iteritems())vì hai lý do. Đầu tiên, frozenset(self)bước này sử dụng lại các giá trị băm được lưu trữ trong từ điển, tiết kiệm các cuộc gọi không cần thiết đến hash(key). Thứ hai, sử dụng itervalues sẽ truy cập trực tiếp các giá trị và tránh nhiều lệnh gọi trình cấp phát bộ nhớ sử dụng theo các mục để tạo thành nhiều bộ giá trị / khóa mới trong bộ nhớ mỗi khi bạn thực hiện tra cứu.


@RaymondHettinger Hãy sửa cho tôi nếu tôi sai, nhưng tôi nghĩ dictbản thân nó không lưu vào bộ nhớ cache các giá trị băm của các khóa của nó - mặc dù các lớp riêng lẻ (như str) có thể và chọn lưu vào bộ nhớ cache của chúng. Ít nhất khi tôi tạo một dictvới các cá thể lớp tùy chỉnh của mình được sử dụng làm khóa, các __hash__phương thức của chúng được gọi trên mọi thao tác truy cập (python 3.4). Cho dù tôi có đúng hay không, tôi không chắc làm thế nào hash(frozenset(self))có thể sử dụng lại các giá trị băm được tính toán trước, trừ khi chúng được lưu vào bộ nhớ đệm bên trong chính các khóa (trong trường hợp đó, hãy hash(frozenset(self.items())sử dụng lại chúng).
tối đa

Đối với điểm thứ hai của bạn về việc tạo tuple (key / value), tôi nghĩ phương thức .items () trả về một dạng xem thay vì một danh sách các bộ giá trị và rằng việc tạo ra dạng xem đó không liên quan đến việc sao chép các khóa và giá trị bên dưới. (Python 3.4 một lần nữa.) Điều đó nói rằng, tôi thấy lợi thế của việc băm chỉ các khóa nếu hầu hết các đầu vào có các khóa khác nhau - bởi vì (1) giá trị băm khá đắt và (2) khá hạn chế để yêu cầu các giá trị có thể băm
tối đa

6
Điều này cũng có khả năng tạo cùng một hàm băm cho hai từ điển khác nhau. Hãy xem xét {'one': 1, 'two': 2}{'one': 2, 'two': 1}
AgDude

Mike Graham trong bình luận của mình nói rằng Tìm ra dict vì bất kỳ lý do nào khác ngoài việc xác định __missing__là một ý tưởng tồi. Bạn nghĩ sao?
Piotr Dobrogost

1
Phân lớp từ dict đã được định nghĩa rõ ràng kể từ Python 2.2. Xem các bộ sưu tập.OrderedDict và bộ sưu tập.Counter để biết các ví dụ từ thư viện chuẩn Python. Nhận xét khác dựa trên niềm tin vô căn cứ rằng chỉ các lớp con của MutableMapping được xác định rõ.
Raymond Hettinger

23

Các câu trả lời đã cho là ổn, nhưng chúng có thể được cải thiện bằng cách sử dụng frozenset(...)thay vì tuple(sorted(...))tạo hàm băm:

>>> import timeit
>>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
4.7758948802947998
>>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
1.8153600692749023

Lợi thế về hiệu suất phụ thuộc vào nội dung của từ điển, nhưng trong hầu hết các trường hợp tôi đã thử nghiệm, việc băm với frozensetnhanh hơn ít nhất 2 lần (chủ yếu vì nó không cần phải sắp xếp).


1
Lưu ý, không cần phải bao gồm cả khóa và giá trị. Giải pháp này sẽ là nhiều nhanh hơn như: hash(frozenset(d)).
Raymond Hettinger

10
@RaymondHettinger: hash(frozenset(d))kết quả là các hàm băm giống nhau cho 2 quân số với các khóa tương tự nhưng giá trị khác nhau!
Oben Sonne

4
Đó không phải là một vấn đề. Công việc của __eq__ là phân biệt giữa các vùng có giá trị khác nhau. Công việc của __hash__ chỉ là giảm không gian tìm kiếm.
Raymond Hettinger

5
Điều đó đúng với khái niệm lý thuyết về hàm băm và ánh xạ nhưng không thực tế đối với bộ nhớ đệm với từ điển dưới dạng tra cứu - không có gì lạ khi các từ điển có khóa tương tự nhưng giá trị khác nhau được chuyển đến một hàm được lưu trong bộ nhớ cache. Trong trường hợp đó, bộ đệm ẩn thực tế biến thành một danh sách thay vì một ánh xạ nếu chỉ các khóa được sử dụng để xây dựng một hàm băm.
Oben Sonne

3
Trong trường hợp đặc biệt của các ký tự có các khóa thụt lề và các giá trị khác biệt, bạn sẽ tốt hơn nếu chỉ lưu trữ một hàm băm dựa trên frozenset(d.itervalues()). Trong trường hợp dicts có phím riêng biệt, frozenset(d)nhiều nhanh hơn và áp đặt không hạn chế về hashability các phím. Cuối cùng, hãy nhớ rằng phương thức dict .__ eq__ sẽ kiểm tra các cặp khóa / giá trị bằng nhau nhanh hơn nhiều mà mọi thứ có thể tính toán băm cho tất cả các bộ giá trị của cặp khóa / giá trị. Việc sử dụng bộ giá trị / khóa cũng có vấn đề vì nó loại bỏ các băm được lưu trữ cho tất cả các khóa (đó là lý do tại sao frozenset(d)quá nhanh).
Raymond Hettinger

11

Một cách triển khai hợp lý, rõ ràng và đơn giản là

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        return hash(tuple(sorted(self._d.iteritems())))

Tại sao lại hợp lý, trong sạch và thẳng thắn như vậy? Tức là vui lòng giải thích sự khác biệt cho các câu trả lời khác, ví dụ: sự cần thiết của __iter____len__.
Karl Richter,

1
@KarlRichter, tôi chưa bao giờ nói điều đó là hợp lý, chỉ là hợp lý trong sạch. ;)
Mike Graham

@KarlRichter, tôi xác định __iter____len__bởi vì tôi phải, vì tôi đang dẫn xuất collections.Mapping; cách sử dụng collections.Mappingđược đề cập khá kỹ trong tài liệu mô-đun bộ sưu tập. Những người khác không cảm thấy cần thiết vì họ đang phát triển dict. Bắt nguồn dicttừ bất kỳ lý do nào khác nhưng để xác định __missing__là một ý tưởng tồi. Thông số kỹ thuật của dict không cho biết cách hoạt động của dict trong trường hợp như vậy, và trên thực tế, điều này sẽ dẫn đến việc có rất nhiều phương thức không phải ảo ít hữu ích hơn nói chung và trong trường hợp cụ thể này sẽ có các phương thức xác minh với hành vi không liên quan.
Mike Graham

7

Tôi tiếp tục quay lại chủ đề này ... Đây là một biến thể khác. Tôi không thoải mái với phân lớp dictđể thêm một __hash__phương thức; Hầu như không có lối thoát nào khỏi vấn đề rằng các dict có thể thay đổi, và tin tưởng rằng chúng sẽ không thay đổi có vẻ là một ý tưởng yếu. Vì vậy, thay vào đó, tôi đã xem xét việc xây dựng một ánh xạ dựa trên một loại nội trang mà bản thân nó là bất biến. Mặc dutuple là một lựa chọn hiển nhiên, nhưng việc truy cập các giá trị trong đó bao hàm một sự sắp xếp và một đường phân giác; không phải là vấn đề, nhưng nó dường như không tận dụng được nhiều sức mạnh của loại mà nó được xây dựng.

Điều gì sẽ xảy ra nếu bạn kẹt khóa, các cặp giá trị thành một frozenset ? Điều đó sẽ đòi hỏi những gì, nó sẽ hoạt động như thế nào?

Phần 1, bạn cần một cách mã hóa của 'item theo cách mà một nhóm người đánh giá sẽ xử lý chúng chủ yếu bằng các khóa của chúng; Tôi sẽ tạo một lớp con nhỏ cho điều đó.

import collections
class pair(collections.namedtuple('pair_base', 'key value')):
    def __hash__(self):
        return hash((self.key, None))
    def __eq__(self, other):
        if type(self) != type(other):
            return NotImplemented
        return self.key == other.key
    def __repr__(self):
        return repr((self.key, self.value))

Chỉ điều đó thôi đã khiến bạn rơi vào khoảng cách xa của một ánh xạ bất biến:

>>> frozenset(pair(k, v) for k, v in enumerate('abcd'))
frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')])
>>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd'))
>>> pair(2, None) in pairs
True
>>> pair(5, None) in pairs
False
>>> goal = frozenset((pair(2, None),))
>>> pairs & goal
frozenset([(2, None)])

Ôi! Thật không may, khi bạn sử dụng các toán tử tập hợp và các phần tử bằng nhau nhưng không cùng một đối tượng; cái nào kết thúc bằng giá trị trả về là không xác định , chúng ta sẽ phải thực hiện thêm một số vòng quay.

>>> pairs - (pairs - goal)
frozenset([(2, 'c')])
>>> iter(pairs - (pairs - goal)).next().value
'c'

Tuy nhiên, việc tra cứu các giá trị theo cách này là cồng kềnh và tệ hơn, tạo ra rất nhiều tập hợp trung gian; điều đó sẽ không làm! Chúng tôi sẽ tạo một cặp khóa-giá trị 'giả' để xử lý nó:

class Thief(object):
    def __init__(self, key):
        self.key = key
    def __hash__(self):
        return hash(pair(self.key, None))
    def __eq__(self, other):
        self.value = other.value
        return pair(self.key, None) == other

Kết quả là ít vấn đề hơn:

>>> thief = Thief(2)
>>> thief in pairs
True
>>> thief.value
'c'

Đó là tất cả những điều kỳ diệu sâu xa; phần còn lại là gói tất cả vào một thứ có giao diện giống như một dict. Vì chúng ta đang phân lớp con frozenset, có giao diện rất khác, nên có khá nhiều phương thức; chúng tôi nhận được một chút trợ giúp từ họ collections.Mapping, nhưng hầu hết công việc là ghi đè các frozensetphương thức cho các phiên bản hoạt động như dicts, thay vào đó:

class FrozenDict(frozenset, collections.Mapping):
    def __new__(cls, seq=()):
        return frozenset.__new__(cls, (pair(k, v) for k, v in seq))
    def __getitem__(self, key):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        raise KeyError(key)
    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            return dict(self.iteritems()) == other
        if len(self) != len(other):
            return False
        for key, value in self.iteritems():
            try:
                if value != other[key]:
                    return False
            except KeyError:
                return False
        return True
    def __hash__(self):
        return hash(frozenset(self.iteritems()))
    def get(self, key, default=None):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        return default
    def __iter__(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def iteritems(self):
        for item in frozenset.__iter__(self):
            yield (item.key, item.value)
    def iterkeys(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def itervalues(self):
        for item in frozenset.__iter__(self):
            yield item.value
    def __contains__(self, key):
        return frozenset.__contains__(self, pair(key, None))
    has_key = __contains__
    def __repr__(self):
        return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()')
    @classmethod
    def fromkeys(cls, keys, value=None):
        return cls((key, value) for key in keys)

mà cuối cùng, nó trả lời câu hỏi của chính tôi:

>>> myDict = {}
>>> myDict[FrozenDict(enumerate('ab'))] = 5
>>> FrozenDict(enumerate('ab')) in myDict
True
>>> FrozenDict(enumerate('bc')) in myDict
False
>>> FrozenDict(enumerate('ab', 3)) in myDict
False
>>> myDict[FrozenDict(enumerate('ab'))]
5

5

Câu trả lời được chấp nhận bởi @Unknown, cũng như câu trả lời của @AlexMartelli hoạt động hoàn toàn tốt, nhưng chỉ dưới các ràng buộc sau:

  1. Giá trị của từ điển phải có thể băm. Ví dụ, hash(hashabledict({'a':[1,2]}))sẽ nâng cao TypeError.
  2. Các phím phải hỗ trợ thao tác so sánh. Ví dụ, hash(hashabledict({'a':'a', 1:1}))sẽ nâng cao TypeError.
  3. Toán tử so sánh trên các khóa áp đặt tổng thứ tự. Ví dụ: nếu hai khóa trong từ điển là frozenset((1,2,3))frozenset((4,5,6)), chúng so sánh không bằng nhau theo cả hai hướng. Do đó, việc sắp xếp các mục của từ điển với các khóa như vậy có thể dẫn đến một thứ tự tùy ý và do đó sẽ vi phạm quy tắc rằng các đối tượng bằng nhau phải có cùng giá trị băm.

Câu trả lời nhanh hơn nhiều của @ObenSonne nâng cao các ràng buộc 2 và 3, nhưng vẫn bị ràng buộc bởi ràng buộc 1 (các giá trị phải có thể băm).

Câu trả lời nhanh hơn của @RaymondHettinger nâng cả 3 ràng buộc vì nó không bao gồm .values()trong phép tính băm. Tuy nhiên, hiệu suất của nó chỉ tốt nếu:

  1. Hầu hết các từ điển (không bằng) cần được băm không giống nhau .keys().

Nếu điều kiện này không được thỏa mãn, hàm băm sẽ vẫn hợp lệ, nhưng có thể gây ra quá nhiều xung đột. Ví dụ: trong trường hợp cực đoan khi tất cả các từ điển được tạo từ một mẫu trang web (tên trường là khóa, người dùng nhập làm giá trị), các khóa sẽ luôn giống nhau và hàm băm sẽ trả về cùng một giá trị cho tất cả các đầu vào . Do đó, một bảng băm dựa vào một hàm băm như vậy sẽ trở nên chậm như một danh sách khi truy xuất một mục ( O(N)thay vì O(1)).

Tôi nghĩ rằng giải pháp sau đây sẽ hoạt động hợp lý ngay cả khi tất cả 4 ràng buộc tôi liệt kê ở trên bị vi phạm. Nó có một lợi thế bổ sung là nó có thể băm không chỉ từ điển mà còn bất kỳ vùng chứa nào, ngay cả khi chúng có các vùng chứa có thể thay đổi lồng nhau.

Tôi đánh giá cao bất kỳ phản hồi nào về điều này, vì cho đến nay tôi chỉ mới thử nghiệm điều này một cách nhẹ nhàng.

# python 3.4
import collections
import operator
import sys
import itertools
import reprlib

# a wrapper to make an object hashable, while preserving equality
class AutoHash:
    # for each known container type, we can optionally provide a tuple
    # specifying: type, transform, aggregator
    # even immutable types need to be included, since their items
    # may make them unhashable

    # transformation may be used to enforce the desired iteration
    # the result of a transformation must be an iterable
    # default: no change; for dictionaries, we use .items() to see values

    # usually transformation choice only affects efficiency, not correctness

    # aggregator is the function that combines all items into one object
    # default: frozenset; for ordered containers, we can use tuple

    # aggregator choice affects both efficiency and correctness
    # e.g., using a tuple aggregator for a set is incorrect,
    # since identical sets may end up with different hash values
    # frozenset is safe since at worst it just causes more collisions
    # unfortunately, no collections.ABC class is available that helps
    # distinguish ordered from unordered containers
    # so we need to just list them out manually as needed

    type_info = collections.namedtuple(
        'type_info',
        'type transformation aggregator')

    ident = lambda x: x
    # order matters; first match is used to handle a datatype
    known_types = (
        # dict also handles defaultdict
        type_info(dict, lambda d: d.items(), frozenset), 
        # no need to include set and frozenset, since they are fine with defaults
        type_info(collections.OrderedDict, ident, tuple),
        type_info(list, ident, tuple),
        type_info(tuple, ident, tuple),
        type_info(collections.deque, ident, tuple),
        type_info(collections.Iterable, ident, frozenset) # other iterables
    )

    # hash_func can be set to replace the built-in hash function
    # cache can be turned on; if it is, cycles will be detected,
    # otherwise cycles in a data structure will cause failure
    def __init__(self, data, hash_func=hash, cache=False, verbose=False):
        self._data=data
        self.hash_func=hash_func
        self.verbose=verbose
        self.cache=cache
        # cache objects' hashes for performance and to deal with cycles
        if self.cache:
            self.seen={}

    def hash_ex(self, o):
        # note: isinstance(o, Hashable) won't check inner types
        try:
            if self.verbose:
                print(type(o),
                    reprlib.repr(o),
                    self.hash_func(o),
                    file=sys.stderr)
            return self.hash_func(o)
        except TypeError:
            pass

        # we let built-in hash decide if the hash value is worth caching
        # so we don't cache the built-in hash results
        if self.cache and id(o) in self.seen:
            return self.seen[id(o)][0] # found in cache

        # check if o can be handled by decomposing it into components
        for typ, transformation, aggregator in AutoHash.known_types:
            if isinstance(o, typ):
                # another option is:
                # result = reduce(operator.xor, map(_hash_ex, handler(o)))
                # but collisions are more likely with xor than with frozenset
                # e.g. hash_ex([1,2,3,4])==0 with xor

                try:
                    # try to frozenset the actual components, it's faster
                    h = self.hash_func(aggregator(transformation(o)))
                except TypeError:
                    # components not hashable with built-in;
                    # apply our extended hash function to them
                    h = self.hash_func(aggregator(map(self.hash_ex, transformation(o))))
                if self.cache:
                    # storing the object too, otherwise memory location will be reused
                    self.seen[id(o)] = (h, o)
                if self.verbose:
                    print(type(o), reprlib.repr(o), h, file=sys.stderr)
                return h

        raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o)))

    def __hash__(self):
        return self.hash_ex(self._data)

    def __eq__(self, other):
        # short circuit to save time
        if self is other:
            return True

        # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first
        # 2) any other situation => lhs.__eq__ will be called first

        # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either
        # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data
        # case 2. neither side is a subclass of the other; self is lhs
        # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented
        # case 3. neither side is a subclass of the other; self is rhs
        # => we can't compare to another type, and the other side already tried and failed;
        # we should return False, but NotImplemented will have the same effect
        # any other case: we won't reach the __eq__ code in this class, no need to worry about it

        if isinstance(self, type(other)): # identifies case 1
            return self._data == other._data
        else: # identifies cases 2 and 3
            return NotImplemented

d1 = {'a':[1,2], 2:{3:4}}
print(hash(AutoHash(d1, cache=True, verbose=True)))

d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True)
print(hash(d))

2

Bạn cũng có thể muốn thêm hai phương thức này để giao thức xử lý v2 hoạt động với các phiên bản băm. Nếu không, cPickle sẽ cố gắng sử dụng lệnh băm .____ setitem____ dẫn đến Lỗi loại. Thật thú vị, với hai phiên bản khác của giao thức, mã của bạn hoạt động tốt.

def __setstate__(self, objstate):
    for k,v in objstate.items():
        dict.__setitem__(self,k,v)
def __reduce__(self):
    return (hashdict, (), dict(self),)

-2

Nếu bạn không đặt số vào từ điển và bạn không bao giờ mất các biến chứa từ điển của mình, bạn có thể làm điều này:

cache[id(rule)] = "whatever"

vì id () là duy nhất cho mọi từ điển

BIÊN TẬP:

Ồ, xin lỗi, đúng vậy trong trường hợp đó những gì những người khác nói sẽ tốt hơn. Tôi nghĩ bạn cũng có thể sắp xếp tuần tự các từ điển của mình dưới dạng một chuỗi, như

cache[ 'foo:bar' ] = 'baz'

Tuy nhiên, nếu bạn cần khôi phục từ điển của mình từ các phím, thì bạn phải làm điều gì đó xấu hơn như

cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

Tôi đoán lợi ích của việc này là bạn sẽ không phải viết nhiều mã.


Hmmm, không; đây không phải là những gì tôi đang tìm kiếm cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache:, Khả năng tạo khóa tự động là điều quan trọng khi tôi muốn sử dụng các khóa làm khóa ngay từ đầu.
SingleNegationElimination

1
Sắp xếp thứ tự các phần có thể ổn, bạn có gợi ý về cách sắp xếp thứ tự cho chúng không? đó là những gì tôi đang tìm kiếm.
SingleNegationElimination
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.