web-dev-qa-db-fra.com

Comment créer un dictionnaire python qui renvoie la clé des clés manquantes dans le dictionnaire au lieu de déclencher KeyError?

Je veux créer un dictionnaire python qui me renvoie la valeur de clé pour les clés manquantes dans le dictionnaire.

Exemple d'utilisation:

dic = smart_dict()
dic['a'] = 'one a'
print(dic['a'])
# >>> one a
print(dic['b'])
# >>> b
54
sorin

dicts ont un __missing__ crochet pour cela:

class smart_dict(dict):
    def __missing__(self, key):
        return key
62
Jochen Ritzel

Pourquoi n'utilisez-vous pas

dic.get('b', 'b')

Bien sûr, vous pouvez sous-classer dict comme d'autres le soulignent, mais je trouve utile de me rappeler de temps en temps que get peut avoir une valeur par défaut!

Si vous voulez essayer le defaultdict, essayez ceci:

dic = defaultdict()
dic.__missing__ = lambda key: key
dic['b'] # should set dic['b'] to 'b' and return 'b'

sauf ... eh bien: AttributeError: ^collections.defaultdict^object attribute '__missing__' is read-only, vous devrez donc sous-classer:

from collections import defaultdict
class KeyDict(defaultdict):
    def __missing__(self, key):
        return key

d = KeyDict()
print d['b'] #prints 'b'
print d.keys() #prints []
25
Daren Thomas

Le premier répondant a mentionné defaultdict, mais vous pouvez définir __missing__ pour toute sous-classe de dict:

>>> class Dict(dict):
        def __missing__(self, key):
            return key


>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d['z']
'z'

De plus, j'aime l'approche du deuxième répondant:

>>> d = dict(a=1, b=2)
>>> d.get('z', 'z')
'z'
13
Raymond Hettinger

Félicitations. Vous aussi, vous avez découvert l'inutilité du type standard collections.defaultdict. Si ce tas caché exécrable de odeur de code offense autant votre sensibilité délicate que la mienne, c'est votre jour de chance StackOverflow.

Grâce à la merveille interdite de la variante à 3 paramètres du composant intégré type(), créer un type de dictionnaire par défaut non inutile est à la fois amusant et rentable.

Quel est le problème avec dict .__ manquant __ ()?

Absolument rien, en supposant que vous aimez l'excès de passe-partout et la stupidité stupéfiante de collections.defaultdict - qui devrait se comporter comme prévu mais ne le fait vraiment pas. Pour être juste, Jochen Ritzel s solution acceptée de sous-classement dict et implémentant méthode __missing__() facultative est une solution de contournement fantastique pour les cas d'utilisation à petite échelle ne nécessitant qu'un seul dictionnaire par défaut.

Mais le passe-partout de cette sorte évolue mal. Si vous vous retrouvez en train d'instancier plusieurs dictionnaires par défaut, chacun avec sa propre logique légèrement différente pour générer des paires clé-valeur manquantes, une solution de rechange automatisée de puissance industrielle est garantie.

Ou du moins Nice. Parce que pourquoi ne pas réparer ce qui est cassé?

Présentation de DefaultDict

En moins de dix lignes de pur Python (hors docstrings, commentaires et espaces blancs), nous définissons maintenant un type DefaultDict initialisé avec un appelable défini par l'utilisateur générant des valeurs par défaut pour les manquants Alors que l'appelable passé au type standard collections.defaultdict accepte inutilement les paramètres no, l'appelable passé à notre type DefaultDict accepte utilement les deux paramètres suivants:

  1. L'instance actuelle de ce dictionnaire.
  2. Clé manquante actuelle pour laquelle générer une valeur par défaut.

Étant donné ce type, la résolution de la question de sorin se réduit à une seule ligne de Python:

>>> dic = DefaultDict(lambda self, missing_key: missing_key)
>>> dic['a'] = 'one a'
>>> print(dic['a'])
one a
>>> print(dic['b'])
b

Santé mentale. Enfin.

Code ou ce n'est pas arrivé

def DefaultDict(keygen):
    '''
    Sane **default dictionary** (i.e., dictionary implicitly mapping a missing
    key to the value returned by a caller-defined callable passed both this
    dictionary and that key).

    The standard :class:`collections.defaultdict` class is sadly insane,
    requiring the caller-defined callable accept *no* arguments. This
    non-standard alternative requires this callable accept two arguments:

    #. The current instance of this dictionary.
    #. The current missing key to generate a default value for.

    Parameters
    ----------
    keygen : CallableTypes
        Callable (e.g., function, lambda, method) called to generate the default
        value for a "missing" (i.e., undefined) key on the first attempt to
        access that key, passed first this dictionary and then this key and
        returning this value. This callable should have a signature resembling:
        ``def keygen(self: DefaultDict, missing_key: object) -> object``.
        Equivalently, this callable should have the exact same signature as that
        of the optional :meth:`dict.__missing__` method.

    Returns
    ----------
    MappingType
        Empty default dictionary creating missing keys via this callable.
    '''

    # Global variable modified below.
    global _DEFAULT_DICT_ID

    # Unique classname suffixed by this identifier.
    default_dict_class_name = 'DefaultDict' + str(_DEFAULT_DICT_ID)

    # Increment this identifier to preserve uniqueness.
    _DEFAULT_DICT_ID += 1

    # Dynamically generated default dictionary class specific to this callable.
    default_dict_class = type(
        default_dict_class_name, (dict,), {'__missing__': keygen,})

    # Instantiate and return the first and only instance of this class.
    return default_dict_class()


_DEFAULT_DICT_ID = 0
'''
Unique arbitrary identifier with which to uniquify the classname of the next
:func:`DefaultDict`-derived type.
'''

La clé ... l'obtenir, clé ? à cette magie arcanique est l'appel à la variante à 3 paramètres de la fonction intégrée type():

type(default_dict_class_name, (dict,), {'__missing__': keygen,})

Cette ligne unique génère dynamiquement une nouvelle sous-classe dict aliasant la méthode facultative __missing__ À l'appelable défini par l'appelant. Notez le manque évident de passe-partout, réduisant l'utilisation de DefaultDict à une seule ligne de Python.

Automatisation pour la victoire flagrante.

7
Cecil Curry

Je suis d'accord que cela devrait être facile à faire, et également facile à configurer avec différentes valeurs par défaut ou fonctions qui transforment une valeur manquante d'une manière ou d'une autre.

Inspiré par Cecil Curry 's answer , je me suis demandé: pourquoi ne pas avoir le générateur par défaut (soit une constante soit un appelable) comme membre de la classe, au lieu de générer des classes différentes tout le temps? Permettez-moi de démontrer:

# default behaviour: return missing keys unchanged
dic = FlexDict()
dic['a'] = 'one a'
print(dic['a'])
# 'one a'
print(dic['b'])
# 'b'

# regardless of default: easy initialisation with existing dictionary
existing_dic = {'a' : 'one a'}
dic = FlexDict(existing_dic)
print(dic['a'])
# 'one a'
print(dic['b'])
# 'b'

# using constant as default for missing values
dic = FlexDict(existing_dic, default = 10)
print(dic['a'])
# 'one a'
print(dic['b'])
# 10

# use callable as default for missing values
dic = FlexDict(existing_dic, default = lambda missing_key: missing_key * 2)
print(dic['a'])
# 'one a'
print(dic['b'])
# 'bb'
print(dic[2])
# 4

Comment ça marche? Pas si difficile:

class FlexDict(dict):
    '''Subclass of dictionary which returns a default for missing keys.
    This default can either be a constant, or a callable accepting the missing key.
    If "default" is not given (or None), each missing key will be returned unchanged.'''
    def __init__(self, content = None, default = None):
        if content is None:
            super().__init__()
        else:
            super().__init__(content)
        if default is None:
            default = lambda missing_key: missing_key
        self.default = default # sets self._default

    @property
    def default(self):
        return self._default

    @default.setter
    def default(self, val):
        if callable(val):
            self._default = val
        else: # constant value
            self._default = lambda missing_key: val

    def __missing__(self, x):
        return self.default(x)

Bien sûr, on peut se demander si l'on veut autoriser la modification de la fonction par défaut après l'initialisation, mais cela signifie simplement supprimer @default.setter et absorbant sa logique dans __init__.

L'activation de l'introspection dans la valeur par défaut actuelle (constante) pourrait être ajoutée avec deux lignes supplémentaires.

2
Axel

Sous-classe dict's __getitem__ méthode. Par exemple, Comment sous-classer correctement dict et remplacer __getitem__ & __setitem __

0
seb