Python: вирок за замовчуванням за замовчуванням?


323

Чи є спосіб defaultdict(defaultdict(int))створити наступний код для роботи?

for x in stuff:
    d[x.a][x.b] += x.c_int

dпотрібно будувати спеціально, залежно від x.aта x.bелементів.

Я можу використовувати:

for x in stuff:
    d[x.a,x.b] += x.c_int

але тоді я не зможу використовувати:

d.keys()
d[x.a].keys()

6
Дивіться подібне запитання Який найкращий спосіб впровадити вкладені словники в Python? . У статті Вікіпедії про автовівіфікацію також є якась корисна інформація .
мартіно

Відповіді:


571

Так ось так:

defaultdict(lambda: defaultdict(int))

Аргумент defaultdict(у даному випадку є lambda: defaultdict(int)) буде викликаний при спробі отримати доступ до ключа, який не існує. Повернене значення буде задано як нове значення цього ключа, що означає, у нашому випадку значення d[Key_doesnt_exist]буде defaultdict(int).

Якщо ви спробуєте отримати доступ до ключа з цього останнього рішення за замовчуванням, тобто d[Key_doesnt_exist][Key_doesnt_exist]він поверне 0, що є значенням повернення аргументу останнього рішення за замовчуванням, тобто int().


7
це чудово працює! ви могли б пояснити раціональне за цим синтаксисом?
Джонатан

37
@Jonathan: Так, аргумент defaultdict(у даному випадку є lambda : defaultdict(int)) буде викликаний, коли ви спробуєте отримати доступ до ключа, який не існує, і його значення буде встановлено як нове значення цього ключа, яке означає в у нашому випадку значення d[Key_dont_exist]буде defaultdict(int), і якщо ви спробуєте отримати доступ до ключа з цього останнього рішення за замовчуванням, тобто d[Key_dont_exist][Key_dont_exist]він поверне 0, що є значенням повернення аргументу останнього, defaultdictтобто int(), сподіваюся, це було корисним.
муад

25
Аргумент до defaultdictмає бути функцією. defaultdict(int)- це словник, а lambda: defaultdict(int)функція, яка повертає словник.
has2k1

27
@ has2k1 Це неправильно. Аргумент за замовчуванням повинен бути викликом. Лямбда - це дзвонити.
Нільс Бом

2
@ RickyLevi, якщо ти хочеш, щоб це працювало, ти просто можеш сказати: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi

51

Параметр конструктору за замовчуванням - це функція, яка буде викликана для побудови нових елементів. Тож давайте використовувати лямбда!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

Оскільки Python 2.7 є ще краще рішення за допомогою Counter :

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Деякі бонусні функції

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Для отримання додаткової інформації див. PyMOTW - Колекції - типи даних про контейнери та Python Documentation - колекції


5
Просто для завершення кола тут ви хочете скористатися, d = defaultdict(lambda : Counter())а не d = defaultdict(lambda : defaultdict(int))конкретно вирішувати проблему, як було поставлено спочатку.
жування

3
@gumption ви не можете просто використовувати d = defaultdict(Counter())немає необхідності для лямбда в цьому випадку
Деб

3
@Деб у вас є невелика помилка - видаліть внутрішні дужки, щоб ви передавали позивний, а не Counterоб'єкт. Тобто:d = defaultdict(Counter)
Діллон Девіс

29

Я вважаю це трохи більш елегантним у використанні partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Звичайно, це те саме, що лямбда.


1
Часткове також краще, ніж лямбда, тому що його можна застосовувати рекурсивно :) Дивіться мою відповідь нижче для загального вкладеного за замовчуванням фабричного методу.
Campi

@Campi вам не потрібен частковий для рекурсивних програм, AFAICT
Clément

10

Для довідки, можна реалізувати загальний вкладений defaultdictзаводський метод через:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()

Глибина визначає кількість вкладеного словника до використання типу, визначеного в default_factory. Наприклад:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')

Чи можете ви навести приклад використання? Не працює так, як я цього очікував. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'кидкиKeyError: 'b'
Девід Маркс

Ей, Девіде, вам потрібно визначити глибину свого словника у прикладі 3 (як ви визначили, що default_factory також буде словником. Nested_defaultdict (dict, 3) буде працювати для вас.
Campi

Це було дуже корисно, дякую! Одне, що я помітив, це те, що це створює за замовчуванням вирок у depth=0, який може не завжди бути бажаним, якщо глибина невідома на момент виклику. Легко фіксується, додавши рядок if not depth: return default_factory()у верхній частині функції, хоча, мабуть, є більш елегантне рішення.
Брендан

9

Попередні відповіді стосувалися того, як зробити дворівневий або n-рівень defaultdict. У деяких випадках потрібно нескінченне:

def ddict():
    return defaultdict(ddict)

Використання:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})

1
Мені це подобається. Це чортово просто, але неймовірно корисно. Дякую!
rosstex

6

Інші правильно відповіли на ваше запитання, як змусити працювати наступне:

for x in stuff:
    d[x.a][x.b] += x.c_int

Альтернативою було б використання кортежів для ключів:

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

Приємне в цьому підході полягає в тому, що він простий і його можна легко розширити. Якщо вам потрібно картографувати три рівні глибиною, просто використовуйте кортеж з трьох елементів для ключа.


4
Це рішення означає, що дістати весь d [xa] не просто, оскільки вам потрібно проаналізувати кожну клавішу, щоб побачити, чи є вона xa як перший елемент кортежу.
Меттью Шинкель

5
Якщо ви хотіли вкласти 3 рівні глибоко, то просто визначте це як 3 рівні: d = засудження за замовчуванням (лямбда: типовий вирок (лямбда: дефолт)
Matthew Schinckel
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.