web-dev-qa-db-fra.com

Comment vérifier avec élégance si un dictionnaire a une structure donnée?

J'ai un dictionnaire avec la structure suivante:

D = {
   'rows': 11,
   'cols': 13,
   (i, j): {
              'meta': 'random string',
              'walls': {
                  'E': True,
                  'O': False,
                  'N': True,
                  'S': True
              }
           }
}
# i ranging in {0 .. D['rows']-1}
# j ranging in {0 .. D['cols']-1}

On me demande de écrire une fonction qui prend un objet arbitraire en argument et vérifie si elle a cette structure Voici ce que j'ai écrit:

def well_formed(L):
    if type(L) != dict:
        return False
    if 'rows' not in L:
        return False
    if 'cols' not in L:
        return False

    nr, nc = L['rows'], L['cols']

    # I should also check the int-ness of nr and nc ...

    if len(L) != nr*nc + 2:
        return False

    for i in range(nr):
        for j in range(nc):
            if not ((i, j) in L
                and 'meta' in L[i, j]
                and  'walls' in L[i, j]
                and type(L[i, j]['meta']) == str
                and type(L[i, j]['walls'])  == dict
                and     'E' in L[i, j]['walls']
                and     'N' in L[i, j]['walls']
                and     'O' in L[i, j]['walls']
                and     'S' in L[i, j]['walls']
                and type(L[i, j]['walls']['E']) == bool
                and type(L[i, j]['walls']['N']) == bool
                and type(L[i, j]['walls']['O']) == bool
                and type(L[i, j]['walls']['S']) == bool):
                return False

    return True

Bien que cela fonctionne, je ne l'aime pas du tout. Existe-t-il un moyen pythonique de le faire?

Je suis autorisé à utiliser uniquement la bibliothèque standard.

17
georgian

Premièrement, je pense que le plus «Pythonique» pourrait être de demander pardon plutôt que d’autorisation - pour vérifier si vous avez besoin d’une propriété si la structure de données en possède une.

Mais d'un autre côté, cela n'aide pas si on vous demande de créer quelque chose pour vérifier que tout est bien formé. :)

Donc, si vous devez vérifier, vous pouvez utiliser quelque chose comme la bibliothèque de schémas pour définir la structure de vos données, puis vérifier les autres structures de données par rapport à ce schéma.

16
bouteillebleu

En Python, l'identité exacte des types impliqués est moins importante que le comportement des valeurs. Pour un use défini d'un tel objet, celui-ci suffirait-il? Cela signifie que L ne doit pas nécessairement être un dict, il ne dispose que d’un support __getitem__; L[(i,j)]['meta'] ne doit pas nécessairement être une str, il doit simplement supporter conversion en chaîne via str(L[(i,j)]['meta']); etc.

Compte tenu de cet assouplissement, j’essayerais simplement d’attraper les erreurs soulevées en tentant de telles actions et je renverrais False le cas échéant. Par exemple,

def well_formed(L):
    try:
        nr = L['rows']
        nc = L['cols']
    except KeyError:
        return False

    try:
        for i in range(nr):
            for j in range(nc):
                str(L[(i,j)]['meta'])
                walls = L[(i,j)]['walls']
                for direction in walls:
                    # Necessary?
                    if direction not in "ENOS":
                        return False
                    if walls[direction] not in (True, False):
                        return False
    except KeyError:
        return False

    return True

Étant donné que tout objet a une valeur booléenne, il semble inutile de tenter bool(walls[direction]); au lieu de cela, si avoir exactement True ou False comme valeur n'est pas une exigence absolue, j’aimerais tout simplement tester la valeur. De même, des murs supplémentaires peuvent ou non être un problème et ne doivent pas être explicitement testés.

7
chepner

Vous pouvez composer une validation comme celle-ci (idée des extracteurs Scala). L'avantage est que la structure du validateur est similaire à la structure à tester.

Un inconvénient est que les nombreux appels de fonctions peuvent le ralentir beaucoup.

class Mapping:
    def __init__(self, **kwargs):
        self.key_values = [KeyValue(k, v) for k, v in kwargs.items()]

    def validate(self, to_validate):
        if not isinstance(to_validate, dict):
            return False

        for validator in self.key_values:
            if not validator.validate(to_validate):
                return False
        return True


class KeyValue:
    def __init__(self, key, value):
        self.key = key
        self.value = value

    def validate(self, to_validate):
        return self.key in to_validate and self.value.validate(to_validate[self.key])


class Boolean:
    def validate(self, to_validate):
        return isinstance(to_validate, bool)


class Integer:
    def validate(self, to_validate):
        return isinstance(to_validate, int)


class String:
    def validate(self, to_validate):
        return isinstance(to_validate, str)


class CustomValidator:
    def validate(self, to_validate):
        if not Mapping(rows=Integer(), cols=Integer()).validate(to_validate):
            return False
        element_validator = Mapping(meta=String(), walls=Mapping(**{k: Boolean() for k in "EONS"}))
        for i in range(to_validate['rows']):
            for j in range(to_validate['cols']):
                if not KeyValue((i, j), element_validator).validate(to_validate):
                    return False
        return True


d = {
    'rows': 11,
    'cols': 13,
}
d.update({(i, j): {
    'meta': 'random string',
    'walls': {
        'E': True,
        'O': False,
        'N': True,
        'S': True
    }
} for i in range(11) for j in range(13)})

assert CustomValidator().validate(d)

Même chose avec isinstance prioritaire (testé avec Python 3.5)

class IsInstanceCustomMeta(type):
    def __instancecheck__(self, instance):
        return self.validate(instance)

def create_custom_isinstance_class(f):
    class IsInstanceCustomClass(metaclass=IsInstanceCustomMeta):
        validate = f
    return IsInstanceCustomClass

def Mapping(**kwargs):
    key_values = [KeyValue(k, v) for k, v in kwargs.items()]

    def validate(to_validate):
        if not isinstance(to_validate, dict):
            return False

        for validator in key_values:
            if not isinstance(to_validate, validator):
                return False
        return True

    return create_custom_isinstance_class(validate)

def KeyValue(key, value):
    return create_custom_isinstance_class(lambda to_validate: key in to_validate and isinstance(to_validate[key], value))

def my_format_validation(to_validate):
    if not isinstance(to_validate, Mapping(rows=int, cols=int)):
        return False
    element_validator = Mapping(meta=str, walls=Mapping(**{k: bool for k in "EONS"}))
    for i in range(to_validate['rows']):
        for j in range(to_validate['cols']):
            if not isinstance(to_validate, KeyValue((i, j), element_validator)):
                return False
    return True

MyFormat = create_custom_isinstance_class(my_format_validation)

d = {
    'rows': 11,
    'cols': 13,
}
d.update({(i, j): {
    'meta': 'random string',
    'walls': {
        'E': True,
        'O': False,
        'N': True,
        'S': True
    }
} for i in range(11) for j in range(13)})

assert isinstance(d, MyFormat)
4
Siphor

Si votre format était plus simple, je serais d'accord avec les autres réponses/commentaires pour utiliser les bibliothèques de validation de schéma existantes, telles que schema et voluptuous . Mais, étant donné que vous devez vérifier un dictionnaire avec des clés de tuples et que les valeurs de ces tuples dépendent des valeurs des autres membres de votre dict, je pense que vous feriez mieux d'écrire votre propre validateur que d'essayer de convaincre un schéma pour s'adapter à votre format.

3
tavnab
from itertools import product

def isvalid(d):
    try:
        for key in product(range(d['rows']), range(d['cols'])):
            sub = d[key]
            assert (isinstance(sub['meta'], str) and
                    all(isinstance(sub['walls'][c], bool)
                        for c in 'EONS'))
    except (KeyError, TypeError, AssertionError):
        return False
    return True

Si la compatibilité avec Python 2 est importante ou s'il est nécessaire d'affirmer qu'il n'y a pas de clé supplémentaire, faites le moi savoir.

1
Alex Hall