Comment implémenter votre propre structure de données en Python

Python fournit un support complet pour l'implémentation de votre propre structure de données à l'aide de classes et d'opérateurs personnalisés. Dans ce didacticiel, vous allez implémenter une structure de données de pipeline personnalisée pouvant effectuer des opérations arbitraires sur ses données. Nous allons utiliser Python 3.

La structure de données du pipeline

La structure de données du pipeline est intéressante car très flexible. Il consiste en une liste de fonctions arbitraires pouvant être appliquées à une collection d'objets et produisant une liste de résultats. Je vais tirer parti de l'extensibilité de Python et utiliser le caractère pipe ("|") pour construire le pipeline..

Exemple en direct

Avant de plonger dans tous les détails, voyons un pipeline très simple en action:

x = étendue (5) | Pipeline () | double | Ω empreinte (x) [0, 2, 4, 6, 8] 

Que se passe t-il ici? Décomposons cela étape par étape. Le premier élément gamme (5) crée une liste d'entiers [0, 1, 2, 3, 4]. Les entiers sont introduits dans un pipeline vide désigné par Pipeline(). Ensuite, une "double" fonction est ajoutée au pipeline, et enfin le cool Ω la fonction termine le pipeline et l'amène à s'autoévaluer. 

L'évaluation consiste à prendre l'entrée et à appliquer toutes les fonctions du pipeline (dans ce cas, la double fonction). Enfin, nous stockons le résultat dans une variable appelée x et l’imprimons..

Classes Python

Python prend en charge les classes et dispose d'un modèle très sophistiqué orienté objet, comprenant l'héritage multiple, les mixins et la surcharge dynamique. Un __init __ () function sert de constructeur qui crée de nouvelles instances. Python prend également en charge un modèle de méta-programmation avancé, que nous ne détaillerons pas dans cet article.. 

Voici une classe simple qui a un __init __ () constructeur qui prend un argument optionnel X (par défaut à 5) et le stocke dans un self.x attribut. Il a aussi un foo () méthode qui renvoie le self.x attribut multiplié par 3:

classe A: def __init __ (self, x = 5): self.x = x def foo (self): retourne self.x * 3 

Voici comment l'instancier avec et sans un argument x explicite:

>>> a = A (2) >>> print (a.foo ()) 6 a = A () print (a.foo ()) 15 

Opérateurs personnalisés

Avec Python, vous pouvez utiliser des opérateurs personnalisés pour vos classes afin d’améliorer la syntaxe. Il existe des méthodes spéciales appelées méthodes "dunder". Le "dunder" signifie "double soulignement". Ces méthodes telles que "__eq__", "__gt__" et "__ou__" vous permettent d'utiliser des opérateurs tels que "==", ">" et "|" avec vos instances de classe (objets). Voyons comment ils travaillent avec la classe A.

Si vous essayez de comparer deux instances différentes de A, le résultat sera toujours False, quelle que soit la valeur de x:

>>> print (A () == A ()) False 

En effet, Python compare les adresses mémoire des objets par défaut. Disons que nous voulons comparer la valeur de x. Nous pouvons ajouter un opérateur "__eq__" spécial qui prend deux arguments, "self" et "other", et compare leur attribut x:

 def __eq __ (self, other): retourne self.x == autre.x 

Vérifions:

>>> print (A () == A ()) True >>> print (A (4) == A (6)) False 

Implémentation du pipeline en tant que classe Python

Maintenant que nous avons couvert les bases des classes et des opérateurs personnalisés en Python, utilisons-les pour implémenter notre pipeline. le __init __ () constructeur prend trois arguments: fonctions, entrée et terminaux. L'argument "functions" est une ou plusieurs fonctions. Ces fonctions sont les étapes du pipeline qui agissent sur les données d'entrée. 

L'argument "input" est la liste des objets sur lesquels le pipeline va opérer. Chaque élément de l'entrée sera traité par toutes les fonctions du pipeline. L'argument "terminaux" est une liste de fonctions. Lorsque l'une d'elles est rencontrée, le pipeline s'autoévalue et renvoie le résultat. Les terminaux ne sont par défaut que la fonction print (en Python 3, "print" est une fonction). 

Notez qu'à l'intérieur du constructeur, un mystérieux "Ω" est ajouté aux terminaux. Je vais expliquer que la prochaine. 

Le constructeur de pipeline

Voici la définition de la classe et le __init __ () constructeur:

classe Pipeline: def __init __ (auto, fonctions = (), entrée = (), terminaux = (impression,)): si hasattr (fonctions, '__call__'): self.functions = [fonctions] else: self.functions = liste (fonctions) self.input = entrée self.terminals = [Ω] + liste (terminaux) 

Python 3 prend entièrement en charge les noms d'identifiant Unicode. Cela signifie que nous pouvons utiliser des symboles sympas tels que "Ω" pour les noms de variables et de fonctions. Ici, j'ai déclaré une fonction d'identité appelée "Ω", qui sert de fonction terminale: Ω = lambda x: x

J'aurais aussi pu utiliser la syntaxe traditionnelle:

def Ω (x): retourne x 

Les opérateurs "__or__" et "__ror__"

Voici le coeur de la classe Pipeline. Pour utiliser le "|" (symbole de conduite), nous devons remplacer deux opérateurs. Le "|" Le symbole est utilisé par Python pour bitwise ou des entiers. Dans notre cas, nous souhaitons le surcharger pour mettre en œuvre un chaînage de fonctions ainsi que pour alimenter les entrées au début du pipeline. Ce sont deux opérations distinctes.

L'opérateur "__ror__" est appelé lorsque le deuxième opérande est une instance de Pipeline tant que le premier opérande ne l'est pas. Il considère le premier opérande comme entrée et le stocke dans la auto.input attribut, et retourne l’instance Pipeline (le self). Cela permet d'enchaîner plus de fonctions plus tard.

def __ror __ (self, input): self.input = input return self 

Voici un exemple où le __ror __ () l'opérateur serait invoqué: 'bonjour il' | Pipeline()

L'opérateur "__ou__" est appelé lorsque le premier opérande est un pipeline (même si le deuxième opérande est également un pipeline). Il accepte que l'opérande soit une fonction appelable et affirme que l'opérande "func" est effectivement appelable.. 

Ensuite, il ajoute la fonction à la auto.fonctions attribue et vérifie si la fonction est l’une des fonctions du terminal. S'il s'agit d'un terminal, l'ensemble du pipeline est évalué et le résultat est renvoyé. Si ce n'est pas un terminal, le pipeline lui-même est renvoyé.

def __ou __ (self, func): assert (hasattr (func, '__call__')) self.functions.append (func) si func dans self.terminals: return self.eval () return self 

Évaluation du pipeline

À mesure que vous ajoutez de plus en plus de fonctions non terminales au pipeline, rien ne se passe. L'évaluation proprement dite est différée jusqu'à la eval () méthode est appelée. Cela peut se produire en ajoutant une fonction de terminal au pipeline ou en appelant eval () directement. 

L’évaluation consiste à parcourir toutes les fonctions du pipeline (y compris la fonction de terminal s’il en existe une) et à les exécuter dans l’ordre dans la sortie de la fonction précédente. La première fonction du pipeline reçoit un élément d'entrée.

def eval (self): result = [] pour x dans self.input: pour f dans self.functions: x = f (x) result.append (x) return result 

Utilisation efficace du pipeline

L’un des meilleurs moyens d’utiliser un pipeline est de l’appliquer à plusieurs ensembles d’entrées. Dans l'exemple suivant, un pipeline sans entrées ni fonctions de terminal est défini. Il a deux fonctions: l'infâme double fonction nous avons défini plus tôt et la norme math.floor

Ensuite, nous lui fournissons trois entrées différentes. Dans la boucle intérieure, nous ajoutons le Ω fonction du terminal lorsque nous l’appelons pour collecter les résultats avant de les imprimer:

p = pipeline () | double | math.floor à entrer dans ((0.5, 1.2, 3.1), (11.5, 21.2, -6.7, 34.7), (5, 8, 10.9)): résultat = entrée | p | Ω impression (résultat) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21] 

Vous pouvez utiliser le impression fonction du terminal directement, mais chaque élément sera imprimé sur une ligne différente:

keep_palindromes = lambda x: (p pour p dans x si p [:: - 1] == p) keep_longer_than_3 = lambda x: (p pour p dans x si len (p)> 3) p = Pipeline () | keep_palindromes | keep_longer_than_3 | liste (('aba', 'abba', 'abcdef'),) | p | print ['abba'] 

Améliorations futures

Quelques améliorations peuvent rendre le pipeline plus utile:

  • Ajoutez du streaming afin qu'il puisse fonctionner sur des flux d'objets infinis (par exemple, lire des fichiers ou des événements réseau).
  • Fournissez un mode d'évaluation dans lequel la totalité de l'entrée est fournie sous la forme d'un objet unique afin d'éviter la solution de contournement fastidieuse consistant à fournir une collection d'un élément.
  • Ajouter diverses fonctions de pipeline utiles.

Conclusion

Python est un langage très expressif et est bien équipé pour concevoir votre propre structure de données et vos types personnalisés. La possibilité de remplacer des opérateurs standard est très puissante lorsque la sémantique se prête à une telle notation. Par exemple, le symbole de tuyau ("|") est très naturel pour un pipeline. 

De nombreux développeurs Python apprécient les structures de données intégrées de Python, telles que les n-uplets, les listes et les dictionnaires. Cependant, la conception et la mise en œuvre de votre propre structure de données peuvent rendre votre système plus simple et plus convivial, en élevant le niveau d'abstraction et en masquant les détails internes aux utilisateurs. Essaie.