Czy istnieje sprytny sposób na przekazanie klucza do fabryki defaultdict?

95

Klasa posiada konstruktor, który przyjmuje jeden parametr:

class C(object):
    def __init__(self, v):
        self.v = v
        ...

Gdzieś w kodzie przydatne jest, aby wartości w dyktandzie znały swoje klucze.
Chcę użyć defaultdict z kluczem przekazanym do wartości domyślnych noworodka:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

Jakieś sugestie?

Benjamin Nitlehoo
źródło

Odpowiedzi:

128

Trudno kwalifikować się jako sprytne - ale podklasy są twoim przyjacielem:

class keydefaultdict(defaultdict):
    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError( key )
        else:
            ret = self[key] = self.default_factory(key)
            return ret

d = keydefaultdict(C)
d[x] # returns C(x)
Jochen Ritzel
źródło
16
To jest dokładnie ta brzydota, której staram się uniknąć ... Nawet używając prostego dyktowania i sprawdzanie istnienia klucza jest znacznie czystsze.
Benjamin Nitlehoo
1
@Paul: a jednak to jest twoja odpowiedź. Brzydota? Daj spokój!
tzot
4
Myślę, że po prostu wezmę ten fragment kodu i umieszczę go w moim spersonalizowanym module narzędzi ogólnych, aby móc go używać, kiedy tylko zechcę. Nie tak brzydko ...
weronika
24
+1 Bezpośrednio odpowiada na pytanie OP i nie wygląda dla mnie „brzydko”. Również dobre rozwiązanie, ponieważ wielu z nich nie zdawał sobie sprawy, że defaultdictjest __missing__()metoda może być pominięte (jak to można w dowolnej podklasy wbudowanych w dictklasie od wersji 2.5).
martineau
7
+1 Ogólnym celem __braknięcia__ jest dostosowanie zachowania brakujących kluczy. Podejście dict.setdefault () wspomniane przez @silentghost również by działało (na plus, setdefault () jest krótka i już istnieje; z drugiej strony ma problemy z wydajnością i nikt nie lubi nazwy "setdefault") .
Raymond Hettinger
26

Nie, nie ma.

defaultdictRealizacja nie może być skonfigurowany do przekazywania brakuje keydo default_factoryout-of-the-box. Jedyną opcją jest zaimplementowanie własnej defaultdictpodklasy, zgodnie z sugestią @JochenRitzel powyżej.

Ale to nie jest „sprytne” ani prawie tak czyste, jak byłoby to standardowe rozwiązanie biblioteczne (gdyby istniało). Zatem odpowiedź na twoje zwięzłe pytanie, tak / nie, brzmi wyraźnie „nie”.

Szkoda, że ​​w standardowej bibliotece brakuje tak często potrzebnego narzędzia.

Stuart Berg
źródło
Tak, byłoby lepszym wyborem projektowym, gdyby fabryka wzięła klucz (funkcja jednoargumentowa zamiast pustej). Łatwo jest odrzucić argument, gdy chcemy zwrócić stałą.
YvesgereY
6

Myślę, że wcale nie potrzebujesz defaultdicttutaj. Dlaczego po prostu nie użyć dict.setdefaultmetody?

>>> d = {}
>>> d.setdefault('p', C('p')).v
'p'

To oczywiście spowodowałoby wiele przypadków C. Myślę, że w przypadku problemu wystarczy prostsze podejście:

>>> d = {}
>>> if 'e' not in d: d['e'] = C('e')

Byłoby to szybsze niż ta defaultdictlub jakakolwiek inna alternatywa, o ile widzę.

ETA w odniesieniu do szybkości intestu vs. stosowanie klauzuli try-except:

>>> def g():
    d = {}
    if 'a' in d:
        return d['a']


>>> timeit.timeit(g)
0.19638929363557622
>>> def f():
    d = {}
    try:
        return d['a']
    except KeyError:
        return


>>> timeit.timeit(f)
0.6167065411074759
>>> def k():
    d = {'a': 2}
    if 'a' in d:
        return d['a']


>>> timeit.timeit(k)
0.30074866358404506
>>> def p():
    d = {'a': 2}
    try:
        return d['a']
    except KeyError:
        return


>>> timeit.timeit(p)
0.28588609450770264
SilentGhost
źródło
7
Jest to wysoce marnotrawne w przypadkach, gdy d jest używany wiele razy i rzadko brakuje klucza: C (klucz) stworzy w ten sposób tony niepotrzebnych obiektów do zebrania przez GC. Ponadto w moim przypadku pojawia się dodatkowy ból, ponieważ tworzenie nowych obiektów C jest powolne.
Benjamin Nitlehoo
@Paul: zgadza się. Proponuję wtedy jeszcze prostszą metodę, zobacz moją edycję.
SilentGhost
Nie jestem pewien, czy jest szybszy niż defaultdict, ale tak właśnie robię (zobacz mój komentarz do odpowiedzi THC4k). Miałem nadzieję, że istnieje prosty sposób na obejście faktu, że default_factory nie przyjmuje żadnych argumentów, aby kod był nieco bardziej elegancki.
Benjamin Nitlehoo
5
@SilentGhost: Nie rozumiem - jak to rozwiązuje problem OP? Myślałem, że OP chciał jakiejkolwiek próby czytania, d[key]aby wrócić, d[key] = C(key)jeśli key not in d. Ale twoje rozwiązanie wymaga od niego, aby faktycznie działał i ustawiał d[key]z wyprzedzeniem? Skąd miałby wiedzieć, czego keybędzie potrzebował?
maks.
2
Ponieważ setdefault jest brzydki jak diabli, a defaultdict z kolekcji POWINIEN obsługiwać funkcję fabryczną, która otrzyma klucz. Cóż za zmarnowana szansa ze strony projektantów Pythona!
jgomo3
0

Oto działający przykład słownika, który automatycznie dodaje wartość. Zadanie demonstracyjne polegające na znajdowaniu zduplikowanych plików w / usr / include. Zauważ, że słownik PathDict dostosowywania wymaga tylko czterech wierszy:

class FullPaths:

    def __init__(self,filename):
        self.filename = filename
        self.paths = set()

    def record_path(self,path):
        self.paths.add(path)

class PathDict(dict):

    def __missing__(self, key):
        ret = self[key] = FullPaths(key)
        return ret

if __name__ == "__main__":
    pathdict = PathDict()
    for root, _, files in os.walk('/usr/include'):
        for f in files:
            path = os.path.join(root,f)
            pathdict[f].record_path(path)
    for fullpath in pathdict.values():
        if len(fullpath.paths) > 1:
            print("{} located in {}".format(fullpath.filename,','.join(fullpath.paths)))
gerardw
źródło