Is er een slimme manier om de sleutel door te geven aan defaultdict’s default_factory?

Een klasse heeft een constructor die één parameter nodig heeft:

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

Ergens in de code is het handig voor waarden in een dict om hun sleutels te kennen.
Ik wil een standaarddictaat gebruiken waarbij de sleutel wordt doorgegeven aan de standaardwaarden van pasgeborenen:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

Heeft u suggesties?


Antwoord 1, autoriteit 100%

Het kwalificeert nauwelijks als slim– maar subclassificatie is je vriend:

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)

Antwoord 2, autoriteit 22%

Nee, dat is er niet.

De defaultdict-implementatie kan niet worden geconfigureerd om de ontbrekende keydirect door te geven aan de default_factory. Je enige optie is om je eigen defaultdictsubklasse te implementeren, zoals voorgesteld door @JochenRitzel hierboven.

Maar dat is niet “slim” of bijna zo schoon als een standaard bibliotheekoplossing zou zijn (als die zou bestaan). Het antwoord op uw beknopte ja/nee-vraag is dus duidelijk “Nee”.

Het is jammer dat de standaardbibliotheek zo’n vaak nodig hulpmiddel mist.


Antwoord 3, autoriteit 5%

Ik denk niet dat je defaultdicthier nodig hebt. Waarom gebruik je niet gewoon de dict.setdefault-methode?

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

Dat zou natuurlijk veel instanties van Copleveren. In het geval dat het een probleem is, denk ik dat de eenvoudigere aanpak zal volstaan:

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

Het zou sneller zijn dan het defaultdictof een ander alternatief voor zover ik kan zien.

ETAmet betrekking tot de snelheid van de in-test versus het gebruik van de try-except-clausule:

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

Antwoord 4

Hier is een werkend voorbeeld van een woordenboek dat automatisch een waarde toevoegt. De demonstratietaak bij het vinden van dubbele bestanden in /usr/include. Opmerking: voor het aanpassen van woordenboek PathDictzijn slechts vier regels nodig:

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

Antwoord 5

Een andere manier waarop u mogelijk de gewenste functionaliteit kunt bereiken, is door decorateurs te gebruiken

def initializer(cls: type):
    def argument_wrapper(
        *args: Tuple[Any], **kwargs: Dict[str, Any]
    ) -> Callable[[], 'X']:
        def wrapper():
            return cls(*args, **kwargs)
        return wrapper
    return argument_wrapper
@initializer
class X:
    def __init__(self, *, some_key: int, foo: int = 10, bar: int = 20) -> None:
        self._some_key = some_key
        self._foo = foo
        self._bar = bar
    @property
    def key(self) -> int:
        return self._some_key
    @property
    def foo(self) -> int:
        return self._foo
    @property
    def bar(self) -> int:
        return self._bar
    def __str__(self) -> str:
        return f'[Key: {self.key}, Foo: {self.foo}, Bar: {self.bar}]'

Vervolgens kun je een defaultdictmaken als volgt:

>>> d = defaultdict(X(some_key=10, foo=15, bar=20))
>>> d['baz']
[Key: 10, Foo: 15, Bar: 20]
>>> d['qux']
[Key: 10, Foo: 15, Bar: 20]

De default_factorymaakt nieuwe exemplaren van Xmet de opgegeven
argumenten.

Natuurlijk zou dit alleen nuttig zijn als je weetdat de klasse zal worden gebruikt in een default_factory. Anders zou u, om een ​​individuele klasse te instantiëren, iets moeten doen als:

x = X(some_key=10, foo=15)()

Wat een beetje lelijk is… Als u dit echter wilt vermijden en een zekere mate van complexiteit wilt introduceren, kunt u ook een trefwoordparameter zoals factorytoevoegen aan de argument_wrapperdie generiek gedrag mogelijk maakt:

def initializer(cls: type):
    def argument_wrapper(
        *args: Tuple[Any], factory: bool = False, **kwargs: Dict[str, Any]
    ) -> Callable[[], 'X']:
        def wrapper():
            return cls(*args, **kwargs)
        if factory:
            return wrapper
        return cls(*args, **kwargs)
    return argument_wrapper

Waar je de klas dan als volgt zou kunnen gebruiken:

>>> X(some_key=10, foo=15)
[Key: 10, Foo: 15, Bar: 20]
>>> d = defaultdict(X(some_key=15, foo=15, bar=25, factory=True))
>>> d['baz']
[Key: 15, Foo: 15, Bar: 25]

Other episodes