Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Comment définir correctement le comportement externe d'une classe de filtrage de données

+3 votes

J'ai une classe Selector chargée de déterminer si une donnée passée en paramètre répond à une liste de criteres internes.
Je veux utiliser cette classe de 2 manière:
- soit connaitre la liste des criteres internes correspondant à la donnée passée en paramètre = >liste d'objet
- soit simplement savoir si une donnée correspond aux criteres internes => valeur booléenne

Je m'interroge sur l'API de mon objet: vaut-il mieux faire dans la classe Selector

def get_criteres(self, donnee):
    # Retourne une liste d'objet
    return [critere for criteres in self if critere.match(donnee)]

def match(self, donnee):
    # Retourne un booléen. Si la liste des criteres est vide, renvoie donc False
    return bool(self.get_criteres(donnee))

ou bien simplement faire :

def match(self, donnee):
    # Retourne une liste d'objet, qui peut être interpretee comme un bool
    return [critere for criteres in self if critere.match(donnee)]

# et utiliser cette methode comme ceci
for critere in selector.match(donnee):
    pass
# ou bien
if selector.match(donnee):
    pass

Je penche pour la 2ème solution, mais je me demande si c'est pas gênant que le retour de match() soit alternativement considéré comme un bool ou comme une liste ? En même temps ça me parait assez cohérent avec le duck typing, mais je suis pas trop familier de ce concept de programmation

demandé 30-Mar-2016 par toub (430 points)
edité 30-Mar-2016 par toub

Est-il possible d'avoir un aperçu de la classe Selector?

@Zangror
C'est un truc de ce goût là :

def class Selector(object):
    def __init__(self, criteres)
        self.criteres = criteres

class CritereA(object):
    def __init__(self, value):
        self.value = value

    def match(self, donnee)
        self.value > donnee.donneeA

class CritereA(object):
    def __init__(self, value):
        self.value = value

    def match(self, donnee)
        self.value < donnee.donneeB

class Donnee(object):        
    def __init__(self, donneeA, donneeB):
        self.donneeA = donneeA
        self.donneeB = donneeB

critereA = CritereA(5)
critereB = CritereA(10)
donneeTest = Donnee(7, 12)
selector = Selector([critereA, critereB])
# selector.match(donneeTest) est évalué à True si 
# donneeTest matche critereA <=> (5>7) ou si donneeTest  
# matche critereB <=> (10<12)
# J'ai egalement besoin de récupérer la liste des 
# criteres matchant la donnee (en l’occurrence, ici
# critereB matche donneeTest mais critereA ne matche pas)
result = selector.match(donnee)

ce que je ne comprends pas, c'est si un "succès" doit correspondre à satisfaire au moins un critère ou bien à tous les critères.

@bubulle
Il suffit qu'un des critères matche pour que cela corresponde à un succès. Autrement dit, il suffit que la liste des critères matchant la donnée soit non vide pour que cela correspondent à un succès.

2 Réponses

+3 votes
 
Meilleure réponse

Je pense que la première solution est plus claire et plus propre. Sémantiquement, les deux opérations ne sont pas pareilles, le duck-typing est donc un mauvais prétexte.

Exemple de souci qui pourrait arriver avec la seconde version : supposons que quelqu'un veuille modifier la méthode match et retourner un itérateur sur les critères plutôt qu'une liste, tout les codes employant match comme une valeur booléenne seront cassés, puisque il vaudra toujours True.

Au passage, utiliser un itérateur ici peut permettre d'optimiser ton code pour match dans le cas où il y a beaucoup de critères, et qu'il est donc souvent inutile de les tester tous :

def criteres(self, donnee):
    # Retourne un itérateur sur les critères valides
    return (critere for criteres in self if critere.match(donnee))

def match(self, donnee):
    # Retourne un booléen. Si la liste des criteres est vide, renvoie donc False
    return any(self.criteres(donnee))
répondu 30-Mar-2016 par yoch (2,510 points)
sélectionné 1-Avr-2016 par toub

OK avec la différence sur la sémantique des 2 opérations. J'ai tellement vu le duck typing être justifié à toutes les sauces sur les forums python que je pensais la 2ème solution plus habituelle en python. Peux tu dans ce cas m'expliquer brièvement dans quels cas le duck typing est plus adapté ?

Le duck typing signifie simplement qu'au lieu de se casser la tête avec la notion de type en python, on admet simplement qu'un objet qui supporte une méthode X ou Y (qui a elle une sémantique bien définie, enfin en principe) comme de "type" X-able ou Y-able.

(en python les mots en -able sont très utilisés : iterable, callable, sliceable, etc.)

En ce qui concerne ta seconde solution, elle n'est pas liée à ce concept, mais surtout à la notion de contexte d'évaluation booléen en python. Ce qui me dérange spécialement dans cette solution, c'est le nom que tu donnes à ta méthode (match), alors que cette méthode renvoie une liste de critères, ce qui est différent.

+2 votes

Je pense que tu peux contourner le problème en rajoutant une autre classe dans ton modèle afin de représenter la notion de match. Basiquement, un match consiste en:

  • Un booléen, True si ça matche, sinon False
  • Un attribut matching_criteria qui stockera la liste des critères ayant matché

Du coup, si on reprend ton exemple, on peut arriver à un truc comme:

class Match(object):
    def __init__(self, matching_criteria=[]):
        # si il y a au moin un critère qui matche, match vaut True 
        self.matching_criteria = matching_criteria

    def __bool__(self):
        # un peu de sucre syntaxique pour que tu puisse faire `if match_object`
        # directement, sans avoir à acceder à un attribut de l'objet stocké en dur
        return len(self.matching_criteria) > 0

    __nonzero = __bool__ # compat python 2/3

class Selector(object):
    def __init__(self, criteres)
        self.criteres = criteres

    def match(self, donnee):
        return Match([critere for critere in self.criteres if critere.match(donnee)])

Qui s'utilise comme ça:

critereA = CritereA(5)
critereB = CritereA(10)
donneeTest = Donnee(7, 12)
selector = Selector([critereA, critereB])
match = selector.match(donneeTest)

if match:
    # on affiche la liste des critères ayant matché:
    for c in match.matching_criteria:
        print(c)

EDIT

En l'occurence, la classe Match semble un peu overkill, en effet, il suffirait de retourner directement la liste des critères matchant dans le selecteur et de l'évaluer pour avoir un booléen, ou d'utiliser la liste telle quelle.

Par contre, le fait d'avoir un objet explicite pour stocker le résultat permet de facilement rajouter des fonctionnalités par la suite sans modifier l'API et casser le code existant.

Par exemple, si je veux tester pein de données différentes, et les classer ensuite par pourcentage de match, il me suffit de modifier la classe match:

class Match(object):
    def __init__(self, donnee, matching_criteria, tested_criteria):
        self.donnee = donnee
        self.matching_criteria = matching_criteria
        self.tested_criteria = tested_criteria

    @property
    def score(self):
        """Retourne un score entre 0 et 1, 1 indiquant un match complet, et 0 aucun match"""
        return float(len(self.matching_criteria)) / float(len(test))

    @property
    def partial(self):
        """Return True si le match est partiel"""
        return self.score > 0

    @property
    def complete(self):
        """Return True si le match est complet"""
        return self.score == 1


class Selector(object):
    def __init__(self, criteres)
        self.criteres = criteres

    def match(self, donnee):
        matching = [critere for critere in self.criteres if critere.match(donnee)]
        return Match(donnee, matching, self.criteres)

Dans l'exemple précédent, j'ai également rajouté le stockage de la donnée testée. Par exemple, si l'on souhaite faire un affichage des match, comme pour la page de résultat d'un moteur de recherche, on peut faire:

# partant du principe que mes données à tester sont dans une liste nommée my_data
matches = [selector.match(data) for data in my_data]

# on boucle sur les résultat en mettant les plus pertinents en premier
for match in sorted(matches, key=lambda match: match.score, reverse=True):
    # on affiche la donnée qui a matché
    if match.complete:
        print('{0} correspond totalement!'.format(match.donnee))
    elif match.partial:
        print('{0} correspond partiellement (score de {1})'.format(match.donnee, match.score))
    else:
        print('{0} ne correspond pas du tout'.format(match.donnee))

Cela permet d'avoir toutes les infos nécessaire agrégées au même endroit. Ce n'est pas possible si l'on retourne simplement une liste des critères ayant matché.

répondu 30-Mar-2016 par eliotberriot (678 points)
edité 31-Mar-2016 par eliotberriot

vu que bool([ ]) -> False, je ne comprends pas du tout l'intérêt de la classe Match.
Comme je le comprends, c'est juste inutile.

@bubulle: pas si inutile que ça, on ne valide qu'une fois grâce à cet objet, les critères sont ensuite mémorisés. Utile si les tests sur les critères sont coûteux.

@eliotberriot: return len(self.matching_criteria) > 0 est équivalent à return self.matching_criteria

@yoch
@eliotberriot
je ne vois en effet pas trop l'intérêt de la solution, on pourrait très bien mettre le résultat du match dans une variable, qui mémorise ainsi la liste des criteres. Cette variable est ensuite évaluée soit comme un bool, soit comme une liste; la classe Match présentée ici n'offre pas de possibilité autre

@bubulle, @toub, je viens d'éditer ma réponse pour montrer à quoi peut servir cette classe Match ;)

OK comme ça je vois mieux l'intérêt, mais c'est plus dans le cas d'une évolution future.

...