Python hashable dicts


96

เพื่อเป็นการออกกำลังกายและส่วนใหญ่เพื่อความบันเทิงของตัวเองฉันใช้โปรแกรมแยกวิเคราะห์ packrat แบบย้อนกลับ แรงบันดาลใจสำหรับสิ่งนี้คือฉันต้องการมีความคิดที่ดีขึ้นเกี่ยวกับวิธีที่มาโครที่ไม่ถูกสุขลักษณะจะทำงานในภาษาที่มีลักษณะคล้ายอัลกอล (ตามที่ใช้ประกอบกับภาษาลิสต์ที่ปราศจากวากยสัมพันธ์ที่คุณมักจะพบ) ด้วยเหตุนี้การป้อนข้อมูลที่แตกต่างกันอาจเห็นไวยากรณ์ที่แตกต่างกันดังนั้นผลลัพธ์การแยกวิเคราะห์ที่แคชจึงไม่ถูกต้องเว้นแต่ฉันจะเก็บไวยากรณ์เวอร์ชันปัจจุบันไว้พร้อมกับผลลัพธ์การแยกวิเคราะห์ที่แคชไว้ด้วย ( แก้ไข : ผลที่ตามมาของการใช้คอลเลกชันคีย์ - ค่านี้คือควรจะไม่เปลี่ยนรูป แต่ฉันไม่ได้ตั้งใจที่จะเปิดเผยอินเทอร์เฟซเพื่ออนุญาตให้เปลี่ยนแปลงดังนั้นคอลเลกชันที่เปลี่ยนแปลงได้หรือไม่เปลี่ยนรูปก็ใช้ได้)

ปัญหาคือ python dicts ไม่สามารถปรากฏเป็นคีย์ของ Dict อื่น ๆ ได้ แม้แต่การใช้ทูเพิล (อย่างที่ฉันกำลังทำอยู่) ก็ไม่ช่วยอะไร

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

ฉันเดาว่ามันจะต้องมีสิ่งเหล่านี้อยู่ตลอดทาง ตอนนี้ไลบรารีมาตรฐาน python มีสิ่งที่ฉันต้องการโดยประมาณcollections.namedtupleมีไวยากรณ์ที่แตกต่างกันมาก แต่สามารถใช้เป็นคีย์ได้ ดำเนินการต่อจากเซสชันด้านบน:

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

ตกลง. แต่ฉันต้องสร้างคลาสสำหรับการรวมกันของคีย์ที่เป็นไปได้ในกฎที่ฉันต้องการใช้ซึ่งก็ไม่ได้แย่ขนาดนั้นเพราะกฎการแยกวิเคราะห์แต่ละกฎรู้ว่ามันใช้พารามิเตอร์อะไรดังนั้นคลาสนั้นจึงสามารถกำหนดได้ในเวลาเดียวกัน เป็นฟังก์ชันที่แยกวิเคราะห์กฎ

แก้ไข: ปัญหาเพิ่มเติมnamedtupleของ s คือตำแหน่งที่เคร่งครัด สิ่งสองสิ่งที่ดูเหมือนว่ามันควรจะแตกต่างกันในความเป็นจริงอาจจะเหมือนกัน:

>>> 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: ฉันจะได้dicts ที่สามารถใช้เป็นกุญแจdictของ s อื่น ๆได้อย่างไร?

หลังจากแฮ็คคำตอบเล็กน้อยนี่คือวิธีแก้ปัญหาที่สมบูรณ์กว่าที่ฉันใช้ โปรดทราบว่าสิ่งนี้จะช่วยเพิ่มประสิทธิภาพในการทำให้คำสั่งที่เป็นผลลัพธ์ไม่แน่นอนไม่เปลี่ยนรูปสำหรับวัตถุประสงค์ในทางปฏิบัติ แน่นอนว่ามันยังค่อนข้างง่ายที่จะแฮ็คมันด้วยการโทรdict.__setitem__(instance, key, value)แต่พวกเราทุกคนที่นี่เป็นผู้ใหญ่

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

hashdictจะต้องเปลี่ยนรูปอย่างน้อยหลังจากที่คุณเริ่ม hashing มันดังนั้นทำไมไม่แคชkeyและhashค่าแอตทริบิวต์ของhashdictวัตถุ? ฉันแก้ไข__key()และ__hash__()ทดสอบเพื่อยืนยันว่าเร็วกว่ามาก SO ไม่อนุญาตรหัสที่จัดรูปแบบในความคิดเห็นดังนั้นฉันจะเชื่อมโยงที่นี่: sam.aiki.info/hashdict.py
Sam Watkins

คำตอบ:


73

นี่คือวิธีง่ายๆในการสร้างพจนานุกรมแบบแฮชได้ อย่าลืมว่าอย่ากลายพันธุ์หลังจากฝังในพจนานุกรมอื่นด้วยเหตุผลที่ชัดเจน

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

7
สิ่งนี้ไม่ได้ทำให้แน่ใจว่าeqและhashมีความสอดคล้องกันอย่างมากในขณะที่คำตอบก่อนหน้าของฉันทำผ่านการใช้วิธี __key (ในทางปฏิบัติควรใช้วิธีใดวิธีหนึ่งแม้ว่าวิธีนี้อาจช้าลงโดยการสร้างรายการ itermediate ที่ไม่จำเป็น - แก้ไขได้โดย s / items / iteritems / - สมมติว่า Python 2. * อย่างที่คุณไม่ได้พูด ;-)
Alex Martelli

5
มันน่าจะดีกว่าถ้าใช้ Frozenset แทนที่จะใช้ tuple ในการเรียงลำดับ ไม่เพียง แต่จะเร็วขึ้นเท่านั้น แต่คุณไม่สามารถสรุปได้ว่าคีย์พจนานุกรมสามารถเทียบเคียงได้
asmeurer

1
ดูเหมือนว่าควรมีวิธีหลีกเลี่ยงฟังก์ชันแฮชO(n*log(n))ซึ่งnเป็นจำนวนdictรายการ มีใครรู้บ้างว่าfrozensetฟังก์ชันแฮชของ Python ทำงานแบบ linear time หรือไม่?
Tom Karzes

2
@HelloGoodbye Dict สามารถสร้างแบบนี้dict(key1=value1, key2=value2,...)หรือแบบนี้dict([(key1, value1), (key2, value2),...)])ได้ เช่นเดียวกับเรื่องนี้ การสร้างคุณโพสต์ที่เรียกว่าอักษร
smido

2
@smido: ขอบคุณ hashabledict({key_a: val_a, key_b: val_b, ...})ฉันยังพบว่าคุณก็สามารถโยนที่แท้จริงคือ
HelloGoodbye

64

การแฮชควรไม่เปลี่ยนรูป - ไม่บังคับใช้ แต่ให้ความไว้วางใจว่าคุณจะไม่กลายพันธุ์คำสั่งหลังจากใช้เป็นคีย์ครั้งแรกแนวทางต่อไปนี้จะได้ผล:

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

หากคุณจำเป็นต้องกลายพันธุ์คำสั่งของคุณและยังคงต้องการใช้มันเป็นกุญแจความซับซ้อนจะระเบิดเป็นร้อยเท่า - ไม่ต้องบอกว่ามันทำไม่ได้ แต่ฉันจะรอจนกว่าจะมีข้อบ่งชี้ที่เฉพาะเจาะจงมากก่อนที่ฉันจะเข้าสู่โมราที่เหลือเชื่อ! -)


แน่นอนฉันไม่ต้องการที่จะกลายพันธุ์คำสั่งเมื่อพวกเขาได้รับการเตรียม นั่นจะทำให้อัลกอริทึม packrad ที่เหลือแตกสลาย
SingleNegationElimination

จากนั้นคลาสย่อยที่ฉันแนะนำจะใช้งานได้ - สังเกตว่ามันข้ามปัญหา "ตำแหน่ง" อย่างไร ( ก่อนที่คุณจะแก้ไขคำถามของคุณเพื่อชี้ให้เห็น ;-) ด้วยsortedใน __key ;-)
Alex Martelli

พฤติกรรมขึ้นอยู่กับตำแหน่งของ nametuple ทำให้ฉันประหลาดใจ ฉันเคยเล่นกับมันมาแล้วคิดว่ามันอาจจะยังเป็นวิธีที่ง่ายกว่าในการแก้ปัญหา แต่นั่นก็ทำให้ความหวังของฉัน
หาย

สมมติว่าฉันมีคำสั่งและฉันต้องการส่งให้เป็นแบบแฮชได้ ฉันจะทำอย่างไร
jononomo

@JonCrowell ดูคำถามเหล่านี้เพื่อเป็นแนวคิดและคำชี้แจง: stackoverflow.com/questions/3464061/… , stackoverflow.com/questions/9112300/… , stackoverflow.com/questions/18020074/…
สูงสุด

34

สิ่งที่จำเป็นในการทำให้พจนานุกรมใช้งานได้ตามวัตถุประสงค์ของคุณคือการเพิ่มวิธี __hash__:

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

หมายเหตุการแปลงfrozensetจะใช้ได้กับพจนานุกรมทั้งหมด (กล่าวคือไม่ต้องใช้คีย์ในการจัดเรียง) ในทำนองเดียวกันไม่มีข้อ จำกัด เกี่ยวกับค่าพจนานุกรม

หากมีพจนานุกรมจำนวนมากที่มีคีย์เหมือนกัน แต่มีค่าที่แตกต่างกันจำเป็นต้องให้แฮชคำนึงถึงค่าด้วย วิธีที่เร็วที่สุดคือ:

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

ซึ่งเร็วกว่าfrozenset(self.iteritems())ด้วยเหตุผลสองประการ ครั้งแรกที่frozenset(self)ขั้นตอน reuses hash(key)ค่าแฮเก็บไว้ในพจนานุกรมประหยัดโทรที่ไม่จำเป็นเพื่อ ประการที่สองการใช้itervaluesจะเข้าถึงค่าโดยตรงและหลีกเลี่ยงการเรียกตัวจัดสรรหน่วยความจำจำนวนมากที่ใช้โดยรายการเพื่อสร้างสิ่งที่สำคัญ / ค่าใหม่จำนวนมากในหน่วยความจำทุกครั้งที่คุณทำการค้นหา


@RaymondHettinger แก้ไขฉันถ้าฉันผิด แต่ฉันคิดว่าdictตัวเองไม่ได้แคชค่าแฮชของคีย์ - แม้ว่าแต่ละคลาส (เช่นstr) อาจและเลือกแคชแฮชของพวกเขา อย่างน้อยเมื่อฉันสร้างdictด้วยอินสแตนซ์คลาสที่กำหนดเองของฉันที่ใช้เป็นคีย์__hash__เมธอดของพวกเขาจะถูกเรียกใช้ในทุกการดำเนินการเข้าถึง (python 3.4) ไม่ว่าฉันจะถูกต้องหรือไม่ฉันไม่แน่ใจว่าhash(frozenset(self))จะนำค่าแฮชที่คำนวณล่วงหน้ามาใช้ซ้ำได้อย่างไรเว้นแต่ว่าจะถูกแคชไว้ในคีย์ด้วยตัวเอง (ในกรณีนี้ให้hash(frozenset(self.items())นำกลับมาใช้ใหม่ด้วย)
สูงสุด

สำหรับประเด็นที่สองของคุณเกี่ยวกับการสร้างทูเปิล (คีย์ / ค่า) ฉันคิดว่าเมธอด. items () ส่งคืนมุมมองแทนที่จะเป็นรายการสิ่งที่เพิ่มขึ้นและการสร้างมุมมองนั้นไม่เกี่ยวข้องกับการคัดลอกคีย์และค่าที่อยู่ภายใต้ (Python 3.4 อีกครั้ง) ที่กล่าวว่าฉันเห็นข้อได้เปรียบของการแฮชคีย์หากอินพุตส่วนใหญ่มีคีย์ที่แตกต่างกัน - เนื่องจาก (1) ค่าแฮชค่อนข้างแพงและ (2) ค่อนข้าง จำกัด ที่จะกำหนดให้แฮชค่าได้
สูงสุด

7
นอกจากนี้ยังมีความเป็นไปได้ในการสร้างแฮชเดียวกันสำหรับสองพจนานุกรมที่แตกต่างกัน พิจารณา{'one': 1, 'two': 2}และ{'one': 2, 'two': 1}
AgDude

Mike Graham ในความคิดเห็นของเขาระบุว่าDeriving dict ด้วยเหตุผลอื่นใด แต่การกำหนด__missing__เป็นความคิดที่ไม่ดี คุณคิดอย่างไร?
Piotr Dobrogost

2
Subclassing จาก dict ได้รับการกำหนดไว้อย่างดีตั้งแต่ Python 2.2 ดูคอลเลกชัน OrderDict และคอลเลกชันเคาน์เตอร์สำหรับตัวอย่างจากไลบรารีมาตรฐาน Python ความคิดเห็นอื่น ๆ ขึ้นอยู่กับความเชื่อที่ไม่มีมูลความจริงว่ามีเพียงคลาสย่อยของ MutableMapping เท่านั้นที่กำหนดไว้อย่างดี
Raymond Hettinger

24

คำตอบที่ได้รับนั้นโอเค แต่สามารถปรับปรุงได้โดยใช้frozenset(...)แทนtuple(sorted(...))การสร้างแฮช:

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

ข้อได้เปรียบด้านประสิทธิภาพขึ้นอยู่กับเนื้อหาของพจนานุกรม แต่ในกรณีส่วนใหญ่ที่ฉันได้ทดสอบการแฮชfrozensetจะเร็วขึ้นอย่างน้อย 2 เท่า (ส่วนใหญ่เป็นเพราะไม่จำเป็นต้องเรียงลำดับ)


1
หมายเหตุไม่จำเป็นต้องใส่ทั้งคีย์และและค่า การแก้ปัญหานี้จะมากhash(frozenset(d))ได้เร็วขึ้น:
Raymond Hettinger

10
@RaymondHettinger: hash(frozenset(d))ผลลัพธ์ในแฮชที่เหมือนกันสำหรับ 2 คำสั่งที่มีคีย์ที่เหมือนกัน แต่ค่าต่างกัน!
Oben Sonne

4
นั่นไม่ใช่ปัญหา เป็นหน้าที่ของ __eq__ ในการแยกแยะระหว่างคำสั่งของค่าที่แตกต่างกัน งานของ __hash__ เป็นเพียงการลดพื้นที่การค้นหา
Raymond Hettinger

5
นั่นเป็นความจริงสำหรับแนวคิดเชิงทฤษฎีของแฮชและการแมป แต่ไม่สามารถใช้ได้จริงสำหรับแคชที่มีพจนานุกรมเป็นการค้นหา - ไม่ใช่เรื่องแปลกที่พจนานุกรมที่มีคีย์คล้ายกัน แต่ค่าต่างกันจะถูกส่งไปยังฟังก์ชันแคชแบบมีม ในกรณีนี้แคชจะเปลี่ยนเป็นรายการแทนการแม็ปในทางปฏิบัติหากใช้เฉพาะคีย์สำหรับสร้างแฮช
Oben Sonne

3
ในกรณีพิเศษของการเขียนตามคำบอกที่มีคีย์เยื้องและค่าที่แตกต่างกันคุณจะดีกว่าเพียงแค่จัดเก็บแฮชตามfrozenset(d.itervalues()). ในกรณีที่คำสั่งมีคีย์ที่แตกต่างกันfrozenset(d)จะเร็วกว่ามากและไม่มีข้อ จำกัด ในการแฮชคีย์ สุดท้ายนี้โปรดจำไว้ว่าเมธอด dict .__ eq__ จะตรวจสอบคู่คีย์ / ค่าที่เท่ากันได้เร็วกว่ามากซึ่งสิ่งใดก็ตามที่สามารถคำนวณแฮชสำหรับคู่คีย์ / ค่าทั้งหมดได้ การใช้ tuples ของคีย์ / ค่าก็เป็นปัญหาเช่นกันเนื่องจากจะทำให้แฮชที่จัดเก็บไว้สำหรับคีย์ทั้งหมดออกไป (นั่นคือสาเหตุที่frozenset(d)เร็วมาก)
Raymond Hettinger

12

การใช้งานที่ชัดเจนและตรงไปตรงมาอย่างสมเหตุสมผลคือ

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

เหตุใดจึงสมเหตุสมผลสะอาดและตรงไปตรงมา เช่นกรุณาอธิบายความแตกต่างในการตอบอื่น ๆ เช่นความจำเป็นของและ__iter__ __len__
Karl Richter

1
@KarlRichter ฉันไม่เคยพูดว่ามันสมเหตุสมผลแค่สะอาดพอสมควร ;)
Mike Graham

@KarlRichter ฉันกำหนด__iter__และ__len__เพราะฉันต้องทำตั้งแต่ฉันได้มาcollections.Mapping; วิธีการใช้งานcollections.Mappingมีครอบคลุมอยู่ในเอกสารประกอบโมดูลคอลเลกชัน คนอื่น ๆ dictไม่ได้รู้สึกว่าต้องนับตั้งแต่ที่พวกเขากำลังสืบมา การได้มาdictด้วยเหตุผลอื่นใด แต่เพื่อกำหนด__missing__เป็นความคิดที่ไม่ดี ข้อมูลจำเพาะของ dict ไม่ได้บอกว่า dict ทำงานอย่างไรในกรณีดังกล่าวและในความเป็นจริงสิ่งนี้จะจบลงด้วยการมีวิธีการที่ไม่ใช่เสมือนจำนวนมากซึ่งมีประโยชน์น้อยกว่าโดยทั่วไปและในกรณีนี้จะมีวิธีการขนถ่ายที่มีพฤติกรรมที่ไม่เกี่ยวข้อง
Mike Graham

7

ฉันกลับมาที่หัวข้อนี้เรื่อย ๆ ... นี่คือรูปแบบอื่น ฉันไม่สบายใจกับdictการเพิ่มคลาสย่อยเพื่อเพิ่ม__hash__เมธอด แทบจะไม่มีทางรอดจากปัญหาที่ว่าคำสั่งนั้นไม่แน่นอนและการเชื่อมั่นว่าพวกเขาจะไม่เปลี่ยนแปลงดูเหมือนจะเป็นความคิดที่อ่อนแอ ดังนั้นฉันจึงมองไปที่การสร้างการทำแผนที่ตามประเภทบิวอินที่ไม่เปลี่ยนรูป แม้ว่าtupleจะเป็นทางเลือกที่ชัดเจน แต่การเข้าถึงค่าในนั้นหมายถึงการเรียงลำดับและการแบ่งครึ่ง ไม่ใช่ปัญหา แต่ดูเหมือนจะไม่ได้ใช้ประโยชน์จากพลังของประเภทที่สร้างขึ้นมากนัก

จะเกิดอะไรขึ้นถ้าคุณติดขัดคีย์ให้จับคู่ค่าเป็นfrozenset? สิ่งที่ต้องการมันจะทำงานอย่างไร?

ส่วนที่ 1 คุณต้องมีวิธีการเข้ารหัส 'ไอเท็มในลักษณะที่ Frozenset จะปฏิบัติต่อพวกเขาโดยใช้กุญแจเป็นหลัก ฉันจะสร้างคลาสย่อยเล็กน้อยสำหรับสิ่งนั้น

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

เพียงอย่างเดียวทำให้คุณสามารถคายระยะห่างของการทำแผนที่ที่ไม่เปลี่ยนรูปได้:

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

โธ่! น่าเสียดายที่เมื่อคุณใช้ตัวดำเนินการชุดและองค์ประกอบเท่ากัน แต่ไม่ใช่วัตถุเดียวกัน ค่าใดที่ลงท้ายด้วยค่าที่ส่งคืนไม่ได้กำหนดเราจะต้องผ่านการหมุนเพิ่มเติม

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

อย่างไรก็ตามการค้นหาค่าด้วยวิธีนี้เป็นเรื่องยุ่งยากและที่แย่กว่านั้นคือการสร้างเซตกลางจำนวนมาก ที่จะไม่ทำ! เราจะสร้างคู่คีย์ - ค่า 'ปลอม' เพื่อหลีกเลี่ยง:

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

ซึ่งส่งผลให้เกิดปัญหาน้อยกว่า:

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

นั่นคือทั้งหมดที่เป็นเวทมนตร์ที่ล้ำลึก ส่วนที่เหลือคือการรวมเข้ากับสิ่งที่มีอินเทอร์เฟซเช่นคำสั่ง เนื่องจากเราอยู่ในคลาสย่อยfrozensetซึ่งมีอินเทอร์เฟซที่แตกต่างกันมากจึงมีวิธีการมากมาย เราได้รับความช่วยเหลือเล็กน้อยจากcollections.Mappingแต่งานส่วนใหญ่จะลบล้างfrozensetเมธอดสำหรับเวอร์ชันที่ทำงานเหมือน dicts แทน:

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)

ซึ่งในที่สุดก็ตอบคำถามของฉันเอง:

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

คำตอบที่ยอมรับโดย @Unknown เช่นเดียวกับคำตอบของ @AlexMartelli ทำงานได้ดีอย่างสมบูรณ์ แต่ภายใต้ข้อ จำกัด ต่อไปนี้:

  1. ค่าของพจนานุกรมต้องสามารถซักได้ ยกตัวอย่างเช่นจะยกhash(hashabledict({'a':[1,2]}))TypeError
  2. คีย์ต้องรองรับการดำเนินการเปรียบเทียบ ยกตัวอย่างเช่นจะยกhash(hashabledict({'a':'a', 1:1}))TypeError
  3. ตัวดำเนินการเปรียบเทียบบนคีย์กำหนดลำดับทั้งหมด ตัวอย่างเช่นหากคีย์ทั้งสองในพจนานุกรมมีfrozenset((1,2,3))และfrozenset((4,5,6))เปรียบเทียบไม่เท่ากันในทั้งสองทิศทาง ดังนั้นการจัดเรียงรายการของพจนานุกรมด้วยคีย์ดังกล่าวอาจส่งผลให้เกิดการเรียงลำดับตามอำเภอใจดังนั้นจึงเป็นการละเมิดกฎที่ว่าวัตถุที่เท่ากันจะต้องมีค่าแฮชเท่ากัน

คำตอบที่เร็วกว่ามากโดย @ObenSonne จะยกข้อ จำกัด 2 และ 3 แต่ยังคงถูกผูกไว้ด้วยข้อ จำกัด 1 (ต้องสามารถแฮชค่าได้)

คำตอบที่เร็วกว่า แต่โดย @RaymondHettinger จะยกข้อ จำกัด ทั้ง 3 ข้อเนื่องจากไม่รวม.values()อยู่ในการคำนวณแฮช อย่างไรก็ตามประสิทธิภาพจะดีก็ต่อเมื่อ:

  1. ส่วนใหญ่ของ (ที่ไม่เท่ากัน) .keys()พจนานุกรมที่จะต้องมีการถกกันมีไม่เหมือนกัน

หากเงื่อนไขนี้ไม่เป็นไปตามเงื่อนไขฟังก์ชันแฮชจะยังคงใช้ได้ แต่อาจทำให้เกิดการชนกันมากเกินไป ตัวอย่างเช่นในกรณีที่รุนแรงซึ่งพจนานุกรมทั้งหมดถูกสร้างขึ้นจากเทมเพลตเว็บไซต์ (ชื่อฟิลด์เป็นคีย์การป้อนข้อมูลของผู้ใช้เป็นค่า) คีย์จะเหมือนกันเสมอและฟังก์ชันแฮชจะส่งคืนค่าเดียวกันสำหรับอินพุตทั้งหมด . ด้วยเหตุนี้แฮชแท็กที่อาศัยฟังก์ชันแฮชดังกล่าวจะทำงานช้าพอ ๆ กับรายการเมื่อดึงข้อมูล ( O(N)แทนที่จะเป็นO(1))

ฉันคิดว่าวิธีแก้ปัญหาต่อไปนี้จะทำงานได้ดีพอสมควรแม้ว่าข้อ จำกัด ทั้ง 4 ข้อที่ฉันระบุไว้ข้างต้นจะละเมิดก็ตาม มีข้อได้เปรียบเพิ่มเติมที่ไม่เพียง แต่จะแฮชพจนานุกรมเท่านั้น แต่ยังมีคอนเทนเนอร์ใด ๆ แม้ว่าจะมีคอนเทนเนอร์ที่ไม่แน่นอนซ้อนกันก็ตาม

ฉันขอขอบคุณสำหรับความคิดเห็นใด ๆ เกี่ยวกับเรื่องนี้เนื่องจากจนถึงตอนนี้ฉันได้ทดสอบเพียงเล็กน้อยเท่านั้น

# 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

คุณอาจต้องการเพิ่มสองวิธีนี้เพื่อให้โปรโตคอลการดอง v2 ทำงานกับอินสแตนซ์ hashdict มิฉะนั้น cPickle จะพยายามใช้ hashdict .____ setitem____ ส่งผลให้เกิด TypeError ที่น่าสนใจคืออีกสองเวอร์ชันของโปรโตคอลรหัสของคุณทำงานได้ดี

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

-2

หากคุณไม่ใส่ตัวเลขในพจนานุกรมและคุณจะไม่สูญเสียตัวแปรที่มีพจนานุกรมของคุณคุณสามารถทำได้:

cache[id(rule)] = "whatever"

เนื่องจาก id () ไม่ซ้ำกันสำหรับทุกพจนานุกรม

แก้ไข:

ขออภัยในกรณีนี้สิ่งที่คนอื่นพูดจะดีกว่า ฉันคิดว่าคุณสามารถจัดลำดับพจนานุกรมของคุณเป็นสตริงได้เช่นกัน

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

หากคุณต้องการกู้คืนพจนานุกรมจากคีย์คุณจะต้องทำสิ่งที่น่าเกลียดกว่านี้

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

ฉันเดาว่าข้อดีของสิ่งนี้คือคุณไม่ต้องเขียนโค้ดมากนัก


อืมไม่; นี่ไม่ใช่สิ่งที่ฉันกำลังมองหา: cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cacheความสามารถในการสร้างคีย์แบบไดนามิกมีความสำคัญเมื่อฉันต้องการใช้คำสั่งเป็นคีย์ในตอนแรก
SingleNegationElimination

1
การทำให้คำสั่งเป็นลำดับอาจจะโอเคคุณมีคำแนะนำเกี่ยวกับวิธีการทำให้เป็นอนุกรมหรือไม่ นั่นคือสิ่งที่ฉันกำลังมองหา
SingleNegationElimination
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.