หลายระดับของ 'collection.defaultdict' ใน Python


176

ขอบคุณกลุ่มผู้ใช้ที่ยอดเยี่ยมใน SO ฉันได้ค้นพบความเป็นไปได้ที่มีให้โดยcollections.defaultdictเฉพาะอย่างยิ่งในการอ่านและความเร็ว ฉันทำให้พวกเขาใช้กับความสำเร็จ

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

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

ตอนนี้ใช้งานได้ แต่สิ่งต่อไปนี้ซึ่งเป็นพฤติกรรมที่ต้องการไม่ได้:

d["key4"]["a1"] + 1

ฉันสงสัยว่าฉันควรจะประกาศที่ไหนสักแห่งว่าระดับที่สองdefaultdictเป็นประเภทintแต่ฉันไม่พบว่าจะทำที่ไหนหรืออย่างไร

เหตุผลที่ฉันใช้defaultdictในตอนแรกคือการหลีกเลี่ยงการเริ่มต้นพจนานุกรมสำหรับแต่ละคีย์ใหม่

ข้อเสนอแนะใด ๆ ที่สง่างามมากขึ้น?

ขอบคุณ pythoneers!

คำตอบ:


341

ใช้:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

นี้จะสร้างใหม่เมื่อใดก็ตามที่คีย์ใหม่ที่มีการเข้าถึงในdefaultdict(int)d


2
ปัญหาเดียวก็คือมันจะไม่ดองความหมายmultiprocessingคือไม่มีความสุขเกี่ยวกับการส่งเหล่านี้ไปมา
โนอาห์

19
@Noah: มันจะดองถ้าคุณใช้ฟังก์ชั่นโมดูลระดับชื่อแทนแลมบ์ดา
ระหว่าง

4
@ScienceFriction มีอะไรพิเศษที่คุณต้องการความช่วยเหลือใช่ไหม เมื่อมีการเข้าถึงก็จะเรียกแลมบ์ดาที่จะสร้างใหม่d[new_key] defaultdict(int)และเมื่อd[existing_key][new_key2]เข้าถึงแล้วintจะมีการสร้างใหม่
ระหว่าง

11
นี่มันเจ๋งมาก. ดูเหมือนว่าฉันจะต่ออายุคำสัตย์ปฏิญาณให้เป็น Python ทุกวัน
mVChr

3
กำลังมองหารายละเอียดเพิ่มเติมเกี่ยวกับการใช้วิธีนี้ด้วยmultiprocessingและฟังก์ชั่นโมดูลชื่อระดับคืออะไร? นี้คำถามต่อไปนี้ขึ้น
เซซิเลีย

32

อีกวิธีหนึ่งในการสร้าง pickdable, defaultdict ซ้อนกันคือการใช้วัตถุบางส่วนแทนแลมบ์ดา:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

สิ่งนี้จะทำงานได้เพราะคลาส defaultdict สามารถเข้าถึงได้ทั่วโลกในระดับโมดูล:

"คุณไม่สามารถดองวัตถุบางส่วนเว้นแต่ฟังก์ชั่น [หรือในกรณีนี้ชั้น] มัน wraps สามารถเข้าถึงได้ทั่วโลก ... ภายใต้ __name__ ของมัน (ภายใน __module__ ของมัน)" - กัดกรดห่อฟังก์ชั่นบางส่วน


12

ดูคำตอบของ nosklo ที่นี่เพื่อหาคำตอบทั่วไป

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

การทดสอบ:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

เอาท์พุท:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

ขอบคุณสำหรับลิงค์ @ miles82 (และการแก้ไข @voyager) วิธีนี้เป็นวิธีที่รวดเร็วและปลอดภัยเพียงใด?
Morlock

2
น่าเสียดายที่โซลูชันนี้ไม่ได้รักษาส่วนที่เป็นค่าเริ่มต้นสูงสุดซึ่งเป็นพลังในการเขียนบางอย่างเช่น D ['key'] + = 1 โดยไม่ต้องกังวลเกี่ยวกับการมีอยู่ของคีย์ นั่นคือคุณสมบัติหลักที่ฉันใช้ defaultdict สำหรับ ... แต่ฉันสามารถจินตนาการได้ว่าพจนานุกรมที่มีความลึกมากขึ้นแบบไดนามิกนั้นก็มีประโยชน์เช่นกัน
rschwieb

2
@rschwieb คุณสามารถเพิ่มพลังในการเขียน + = 1 โดยการเพิ่มวิธีการเพิ่ม
spazm

5

ตามคำขอของ @ rschwieb D['key'] += 1เราสามารถขยายหน้าที่แล้วโดยการแทนที่ด้วยการกำหนด__add__วิธีการเพื่อให้พฤติกรรมนี้เป็นเหมือนcollections.Counter()

ครั้งแรกจะถูกเรียกว่าการสร้างค่าว่างใหม่ซึ่งจะถูกส่งผ่านเข้ามา__missing__ เราทดสอบค่านับค่าที่ว่างเปล่าให้เป็น__add__False

ดูการจำลองประเภทตัวเลขสำหรับข้อมูลเพิ่มเติมเกี่ยวกับการแทนที่

from numbers import Number


class autovivify(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

ตัวอย่าง:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

แทนที่จะตรวจสอบอาร์กิวเมนต์เป็นตัวเลข (ไม่ใช่ไพ ธ อนมาก, amirite!) เราสามารถระบุค่าเริ่มต้น 0 แล้วลองดำเนินการ:

class av2(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError

ควรเพิ่มการใช้งานเหล่านี้ไม่ใช่ ValueError
spazm

5

ไปงานปาร์ตี้สาย แต่สำหรับความลึกโดยพลการฉันพบว่าตัวเองทำอะไรแบบนี้:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

เคล็ดลับที่นี่เป็นพื้นเพื่อให้DeepDictอินสแตนซ์ของตัวเองเป็นโรงงานที่ถูกต้องสำหรับการสร้างค่าที่ขาดหายไป ตอนนี้เราสามารถทำสิ่งต่าง ๆ เช่น

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9

1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

ไม่มีข้อผิดพลาดอีกครั้ง ไม่ว่าจะมีกี่ระดับซ้อนกัน ป๊อปยังไม่มีข้อผิดพลาด

DD = DefaultDict ({ "1": 333333})

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