Students Save 30%! Learn & create with unlimited courses & creative assets Students Save 30%! Save Now
Advertisement
  1. Code
  2. Python
Code

Comment définir votre propre structure de données dans Python ?

by
Difficulty:IntermediateLength:MediumLanguages:

French (Français) translation by Stéphane Esteve (you can also view the original English article)

Python contient un ensemble complet pour implémenter vos structures de données personnelles en utilisant des classes et des opérateurs particuliers. Ici, nous allons développer une structure de données modulaire et originale, qui appliquera des opérations arbitraires sur ses données. Nous utiliserons surtout Python 3.

La structure modulaire des données

Ce type de structure est intéressant car il est très flexible. Son fonctionnement consiste en une liste de fonctions arbitraires s'appliquant à une collection d'objet et renvoie une liste de résultats. Il prendra en compte la souplesse du langage Python et l'utilisation du signe pipe ("|") pour architecturer ce pipeline.

Exemple "à chaud"

Avant de plonger dans tous les éléments, observons un simple pipeline en action :

Mais que se passe-t-il ? Analysons-le pas-à-pas. Le premier élément range(5) créait une liste de nombres entiers [0, 1, 2, 3, 4]. Ces entiers sont ensuite digérés par un module vide défini par Pipeline(). Ensuite, une fonction "double" est ajoutée au pipeline, et enfin, la fonction très sexy Ω conclue le tout et déclenche son traitement.

Ce traitement repose sur la prise en compte de l'entrée et l'application de toutes les fonctions décrites dans le pipeline (dans ce cas, seulement la fonction double). Ainsi, nous conservons le résultat dans une variable appelée x et nous l'affichons.

Les classes Python

Python utilise aussi les classes et contient un modèle orienté objet très élaboré, dont les multiples héritages, les mixins et la surcharge dynamique. Une fonction __init__() sert comme constructeur qui prépare chaque instance. Python propose aussi un modèle très poussé de méta-programmation, que nous ne couvrirons pas dans cet article.

Voici une classe simple, contenant un constructeur __init__() dépendant d'un argument optionnel x (par défaut : 5) et le conserve dans un attribut self.x. Une méthode foo() est aussi définie pour renvoyer l'attribut self.x multiplié par 3.

Et maintenant, créons-le avec et sans argument x explicite :

Des opérateurs spéciaux

Avec Python, vous avez accès à des opérateurs spéciaux pour vos classes, afin d'obtenir une syntaxe plus élégante. Il y a plusieurs méthodes, dites "dunder". "Dunder" signifiant "double underscore". Ces méthodes comme "__eq__", "__gt__" et "__or__" reviennent à utiliser des opérateurs comme "==", ">" et "|" au sein de vos instances de classes (les objets). Voyons leur fonctionnement sur la classe A.

Si vous tentez de comparer deux différentes instances de A l'une à l'autre, le résultat sera toujours False, quelle que soit la valeur de x :

En fait, Python compare par défaut les adresses mémoire des objets. Disons que nous voulons changer ce comportement par défaut, en évaluant seulement la valeur de x. Nous ajouterons alors un opérateur "__eq__" qui contient deux arguments, "self" et "other" et évalue l'égalité entre leur attribut x, comme ceci :

Vérifions :

Construire un pipeline avec une classe Python

Alors que nous avons parcouru les bases des classes et de leurs opérateurs dans Python, adoptons-les pour construire notre pipeline. Le constructeur __init__() devra prendre trois arguments : les fonctions, l'entrée et fonctions terminales. L'argument des "fonctions" peut être une ou plusieurs fonctions. Ces fonctions constitueront les différents étages du pipeline, agissant chacune sur l'entrée des données.

L'argument "input" sera la liste des objets sur lequel opère le pipeline. Chaque élément de cette entrée sera traitée par toutes les fonctions de ce bloc. L'argument des fonctions "terminales" produit une liste de fonctions, dès lors que l'une d'entre elles s'exécute, le pipeline se reconstitue et renvoie le résultat. Par défaut, les fonctions terminales relèvent par défaut de la fonction print (eh oui, en Python 3, "print" est une fonction).

Veuillez noter qu'au sein du constructeur, la mystérieuse fonction "Ω" a été insérée dans les fonctions terminales. Je m'en expliquerai plus tard.

Le constructeur du Pipeline

Voici la définition de la classe et son constructeur __init__() :

Python 3 supporte complètement le format Unicode lors de la déclaration de noms. En gros, on peut tout à fait utiliser le symbole "Ω" comme variable ou fonction. Ici, je définie la fonction "Ω", selon ces termes : Ω = lambda x: x

J'aurai pu aussi conserver la syntaxe traditionnelle :

Les opérateurs "__or__" et "__ror__"

Nous voici au cœur de la classe Pipeline. Afin d'employer le "|" (symbole pipe), nous avons besoin de requalifier quelques opérateurs. Le symbole "|" est s'applique aux opérations sur les bits ou sur les nombres entiers. Dans notre cas, nous souhaitons contourner son utilisation classique afin d'y implémenter le chaînage de fonctions, tout en alimentant tout le pipeline avec les données en entrée. Le tout en deux opérations distinctes.

L'opérateur "__ror__" est sollicité dès que le deuxième élément fait partie d'une instance en cours, tant que le premier ne l'est pas. Il évalue le premier élément de la chaîne comme l'entrée et le conserve dans l'attribut self.input. Puis, il renvoie l'instance en cours (le "self"). Ainsi, ce processus rend le chaînage de plusieurs fonctions possible.

Observez cet exemple où l'opérateur __ror__() peut être appelé de cette façon : 'hello there' | Pipeline()

L'opérateur "__or__" est engagé dès que le premier élément est un Pipeline (même si le second élément est aussi un Pipeline). Il considère cet élément comme une fonction exécutable et s'assure que l'élément "func" est effectivement exécutable.

Ainsi, il rajoute la fonction dans l'attribut self.functions et examine si elle fait partie d'une des actions finales. Si c'est le cas, alors l'ensemble du pipeline est traité et le résultat est renvoyé. Sinon, seul le pipeline sera retourné.

Evaluer le Pipeline

A mesure que vous ajouter davantage de fonctions sans but, rien de particulier se manifeste. Le calcul en cours est chaque fois reporté, jusqu'à ce que la méthode eval() soit appelée. Cette phase se produit soit en ajoutant un méthode terminale, soit par l'appel d'eval() directement.

Le traitement procède par itération à travers toutes les fonctions du pipeline (y compris la fonction finale, si elle est présente) et s'exécutent dans l'ordre des résultats obtenus par la fonction antérieure. Seule la toute première fonction de la chaîne reçoit un élément en entrée.

Du bon usage d'un Pipeline

L'une des meilleures façons d'exploiter un pipeline consiste à s'en servir dans de multiples séries de données en entrée. Dans l'exemple suivant, un pipeline est créé  sans aucune entrée et sans fonction finale. Il y a deux fonctions : l'ignoble fonction double que nous avions créé plus tôt et la fonction standard math.floor.

Puis, nous la munissons de trois entrées différentes. Dans la boucle interne, nous rajoutons la fonction finale Ω  afin de collecter les résultats avant de les afficher :

Vous pourriez recourir à la fonction finale print directement, mais du coup, chaque élément serait affiché sur une ligne différente :

Possibilités d'améliorations

Quelques éléments de perfectionnement sont possibles pour parfaire ce pipeline :

  • Ajouter la prise en compte de flux de données, afin qu'il puisse intervenir aussi dans des flux continus d'objets (par ex : lire les contenus de fichiers ou capturer des événements du réseau).
  • Proposer un mode évaluatif où la totalité de l'entrée serait requalifiée comme un seul objet et éviter cette lourdeur incessante qui produit une collection malgré un seul élément.
  • Rajouter quantité de fonctions aussi variées que riches au pipeline.

Conclusion

Python est un langage de programmation suffisamment expressif et tellement enrichi, pour dessiner sa propre structure de données et des typages spéciaux. Sa capacité à contrevenir à la définition de ses propres opérateurs standards est extrêmement puissante dès lors que ses signes se prêtent à de telles notations. Par exemple, quoi de plus naturel que l'usage du symbole pipe ("|") pour déclencher un pipeline.

Nombreux sont les développeurs Python a apprécié les structures pré-établies du langage comme les tuples, les listes et les dictionnaires. En revanche, esquisser et construire votre structure de données personnelle permettra à votre système d'être plus simple et accessible à tous, gravissant les niveaux d'abstraction les plus élevés, tout en camouflant la cuisine interne aux utilisateurs. Donnez-lui une chance.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.