Classe, Héritage, calcule d’une distance

On veut calculer la distance entre un produit et un utilisateur. En première intention, la distance entre deux produits est calculée en fonction de ses attributs et la distance entre deux utilisateurs est calculée en fonction de leurs achats. La pertinence de la distance n’est pas le sujet ici mais quoi coder et dans quelle classe.

Un peu de vocabulaire :

  • une méthode est une fonction d’une classe

  • un attribut est une variable d’une classe

  • le constructeur ou la méthode __init__ est appelé implicitement lorsque l’objet est créé

Classe Product

On lui adjoint trois attributs, un identifiant, un prix, une category.

[9]:
class Product:
    def __init__(self, identifiant, prix, category):
        self.identifiant = identifiant
        self.prix = prix
        self.category = category

    def __repr__(self):
        return f"P:{self.identifiant}:{self.prix}:{self.category}"

    # def __eq__(self, p):
    #     # Cet méthode définit le sens de l'opérateur ==.
    #     # Par défaut, python vérifie que les deux variables comparées désignent
    #     # la même instance et non leur contenu.
    #     return self.identifiant == p.identifiant
    def distance(self, p):
        return abs(self.prix - p.prix) * (1 if self.category == p.category else 10)


p = Product(1, 45, "livre")
p
[9]:
P:1:45:livre
[10]:
p1 = Product(1, 1, 1)
p2 = Product(1, 1, 1)
p3 = p1
p1 == p2, p1 == p3
[10]:
(False, True)

Classe utilisateur

[11]:
import numpy as np


class User:
    def __init__(self, identifiant):
        self.identifiant = identifiant
        self.products = []

    def bought(self, p):
        self.products.append(p)

    def __repr__(self):
        return f"U:{self.identifiant}:{len(self.products)}"

    def distance(self, user):
        # distance entre self et user
        mat = np.empty((len(self.products), len(user.products)), dtype=float)
        for i, p1 in enumerate(self.products):
            for j, p2 in enumerate(user.products):
                mat[i, j] = p1.distance(p2)
        return min(mat.min(axis=1).sum(), mat.min(axis=0).sum())


u1 = User(11)
u1.bought(Product(1, 45, "livre"))
u2 = User(12)
u2.bought(Product(1, 40, "livre"))
u2.bought(Product(1, 30, "disque"))
u1.distance(u2)
[11]:
5.0

Classe Base

La distance calculée sur les utilisateurs peut être calculée de la même façon sur les produit si la classe Product sait quels utilisateurs les ont acheté. De cette façon, on peut dire que deux produits sont proches s’ils sont achetés par les mêmes utilisateurs. Comme ce calcul est le même, il est tentant de créer une classe commune aux produits et aux utilisateurs. Les classes Product et User vont hériter de cette nouvelle classe.

On distance donc :

  • la méthode distance qui est la distance décrite ci-dessus,

  • la méthode similarity qui compare deux produits à partir de leurs attributs.

Les instructions print permettent de suivre quelle méthode est appelée.

[12]:
class Base:
    def __init__(self, identifiant):
        self.identifiant = identifiant
        self.container = []

    def add(self, b):
        self.container.append(b)

    def similarity(self, b):
        # Déclencher une exception NotImplementedError est une façon de dire
        # que la méthode existe mais qu'elle doit être redéfinit ou surchargée dans la
        # classe qui hérite de la classe Base.
        raise NotImplementedError(
            f"il faut surcharger cette méthode pour le type {type(self)}"
        )

    def distance(self, user):
        # distance entre self et user
        print(f"Base:distance:{self.identifiant}:{user.identifiant}")
        mat = np.empty((len(self.container), len(user.container)), dtype=float)
        for i, p1 in enumerate(self.container):
            for j, p2 in enumerate(user.container):
                mat[i, j] = p1.similarity(p2)
        return min(mat.min(axis=1).sum(), mat.min(axis=0).sum())


# Maintenant les héritages


class Product(Base):
    def __init__(self, identifiant, prix, category):
        Base.__init__(self, identifiant)
        self.prix = prix
        self.category = category

    def similarity(self, p):
        # On redéfinit la méthode similarity dans la classe Product car elle lui est propre.
        print(f"Product:similarite:{self.identifiant}:{p.identifiant}")
        return abs(self.prix - p.prix) * (1 if self.category == p.category else 10)


class User(Base):
    def bought(self, p):
        self.add(p)

    def similarity(self, u):
        # On redéfinit la méthode similarity de sorte qu'elle appelle la méthode distance.
        # car elle est appelée par la méthode Product.distance.
        print(f"User:similarity:{self.identifiant}:{u.identifiant}")
        return self.distance(u)


p1 = Product(1, 45, "livre")
p2 = Product(2, 40, "livre")
p3 = Product(3, 30, "disque")

u1 = User(11)
u1.bought(p1)
u2 = User(12)
u2.bought(p2)
u2.bought(p3)

p1.add(u1)
p2.add(u2)
p3.add(u2)

Toujours la distance entre deux utilisateurs.

[13]:
u1.distance(u2)
Base:distance:11:12
Product:similarite:1:2
Product:similarite:1:3
[13]:
5.0

Et maintenant la distance entre deux produits définie à partir des utilisateurs ayant acheté des produits.

[14]:
p1.distance(p2)
Base:distance:1:2
User:similarity:11:12
Base:distance:11:12
Product:similarite:1:2
Product:similarite:1:3
[14]:
5.0
[ ]:


Notebook on github