Comment implémenter une pile Python – Real Python

By | juin 5, 2019

Cours Python en ligne

Avez-vous entendu parler des piles et vous êtes demandé ce qu'elles sont? Vous avez une idée générale mais vous vous demandez comment implémenter une pile Python? Vous êtes arrivé au bon endroit!

Dans ce tutoriel, vous apprendrez:

  • Comment reconnaître quand une pile est un bon choix pour les structures de données
  • Comment décider quelle implémentation est la meilleure pour votre programme
  • Quelles sont les considérations supplémentaires à prendre à propos des piles dans un environnement de threading ou de multitraitement?

Ce tutoriel est destiné aux pythonistes qui savent manipuler des scripts et savent ce qu’il en coûte. liste est et comment l'utiliser, et se demandent comment implémenter des piles Python.

Qu'est-ce qu'une pile?

Une pile est une structure de données qui stocke les éléments de manière dernier entré / premier sorti. Ceci est souvent appelé LIFO. Cela contraste avec une file d'attente qui stocke les éléments de manière FIFO (First-In / First-Out).

Il est probablement plus facile de comprendre une pile si vous pensez à un cas d’utilisation que vous connaissez probablement: annuler fonctionnalité dans votre éditeur.

Imaginons que vous modifiez un fichier Python afin que nous puissions examiner certaines des opérations que vous effectuez. Tout d'abord, vous ajoutez une nouvelle fonction. Cela ajoute un nouvel élément à la pile d'annulation:

Vous pouvez voir que la pile a maintenant un Ajouter une fonction opération sur elle. Après avoir ajouté la fonction, vous supprimez un mot d'un commentaire. Ceci est également ajouté à la pile d'annulation:

Remarquez comment Supprimer le mot l'élément est placé sur la pile. Enfin, vous indenter un commentaire pour qu’il soit correctement aligné:

Vous pouvez voir que chacune de ces commandes est stockée dans une pile d'annulation, chaque nouvelle commande étant placée en haut. Lorsque vous travaillez avec des piles, l’ajout de nouveaux éléments comme ceci s’appelle pousser.

Maintenant que vous avez décidé d’annuler ces trois modifications, vous devez appuyer sur la commande d'annulation. Il prend l'élément en haut de la pile, qui indentait le commentaire, et le supprime de la pile:

Votre éditeur annule le retrait et la pile d'annulation contient maintenant deux éléments. Cette opération est le contraire de pousser et est communément appelé pop.

Lorsque vous appuyez à nouveau sur Annuler, l'élément suivant est extrait de la pile:

Cela supprime le Supprimer le mot élément, ne laissant qu'une seule opération sur la pile.

Enfin, si vous frappez annuler une troisième fois, le dernier élément sera retiré de la pile:

La pile d'annulation est maintenant vide. Frappe annuler à nouveau après cela n'aura aucun effet car votre pile d'annulation est vide, du moins dans la plupart des éditeurs. Vous verrez ce qui se passe lorsque vous appelez .pop() sur une pile vide dans les descriptions d'implémentation ci-dessous.

Implémentation d'une pile Python

Il existe plusieurs options lorsque vous implémentez une pile Python. Cet article ne couvrira pas tous les sujets, mais seulement ceux de base qui répondront à presque tous vos besoins. Vous vous concentrerez sur l'utilisation de structures de données faisant partie de la bibliothèque Python, plutôt que sur l'écriture de vos propres logiciels ou l'utilisation de packages tiers.

Vous examinerez les implémentations de pile Python suivantes:

  • liste
  • collections.deque
  • queue.LifoQueue

En utilisant liste créer une pile de python

Le intégré liste La structure que vous utilisez fréquemment dans vos programmes peut être utilisée comme une pile. Au lieu de .pousser(), vous pouvez utiliser .ajouter() pour ajouter de nouveaux éléments au sommet de votre pile, tout en .pop() supprime les éléments de l'ordre LIFO:

>>>

>>> ma pile = []

>>> ma pile.ajouter('une')
>>> ma pile.ajouter('b')
>>> ma pile.ajouter('c')

>>> ma pile
['a', 'b', 'c']

>>> ma pile.pop()
'c'
>>> ma pile.pop()
'b'
>>> ma pile.pop()
'une'

>>> ma pile.pop()
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
IndexError: pop de liste vide

Vous pouvez voir dans la dernière commande qu'un liste va soulever une IndexError si vous appelez .pop() sur une pile vide.

liste a l'avantage d'être familier. Vous savez comment cela fonctionne et vous l'avez probablement déjà utilisé dans vos programmes.

Malheureusement, liste présente quelques inconvénients par rapport à d’autres structures de données que vous examinerez. Le plus gros problème est qu'il peut rencontrer des problèmes de vitesse à mesure qu'il grandit. Les articles dans un liste sont stockés dans le but de fournir un accès rapide à des éléments aléatoires dans le liste. À un niveau élevé, cela signifie que les éléments sont stockés les uns à côté des autres dans la mémoire.

Si votre pile dépasse la taille du bloc de mémoire qui la contient actuellement, Python doit alors effectuer certaines allocations de mémoire. Cela peut conduire à certains .ajouter() les appels prennent beaucoup plus de temps que les autres.

Il y a aussi un problème moins grave. Si tu utilises .insérer() pour ajouter un élément à votre pile à une position autre que la fin, cela peut prendre beaucoup plus de temps. Ce n'est normalement pas quelque chose que vous feriez avec une pile, cependant.

La prochaine structure de données vous aidera à résoudre le problème de réaffectation que vous avez rencontré avec liste.

En utilisant collections.deque créer une pile de python

le collections le module contient deque, ce qui est utile pour créer des piles Python. deque se prononce «pont» et signifie «file d'attente à double extrémité».

Vous pouvez utiliser les mêmes méthodes sur deque comme vous l'avez vu ci-dessus pour liste, .ajouter(), et .pop():

>>>

>>> de collections importation deque
>>> ma pile = deque()

>>> ma pile.ajouter('une')
>>> ma pile.ajouter('b')
>>> ma pile.ajouter('c')

>>> ma pile
deque (['a', 'b', 'c'])

>>> ma pile.pop()
'c'
>>> ma pile.pop()
'b'
>>> ma pile.pop()
'une'

>>> ma pile.pop()
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
IndexError: pop d'un deque vide

Cela semble presque identique à la liste exemple ci-dessus. À ce stade, vous vous demandez peut-être pourquoi les principaux développeurs de Python créeraient deux structures de données identiques.

Pourquoi avoir deque et liste?

Comme vous l'avez vu dans la discussion sur liste ci-dessus, il a été construit sur des blocs de mémoire contiguë, ce qui signifie que les éléments de la liste sont stockés les uns à côté des autres:

Structure mémoire d'une liste mettant en oeuvre une pile

Cela fonctionne très bien pour plusieurs opérations, comme l’indexation dans le fichier. liste. Obtenir ma liste[3] est rapide, car Python sait exactement où chercher dans la mémoire pour le trouver. Cette disposition de la mémoire permet également aux tranches de bien fonctionner sur les listes.

La disposition de la mémoire contiguë est la raison pour laquelle liste peut-être besoin de prendre plus de temps pour .ajouter() certains objets que d'autres. Si le bloc de mémoire contiguë est plein, il devra obtenir un autre bloc, ce qui peut prendre beaucoup plus de temps qu'une normale. .ajouter():

Structure de la mémoire d'une liste poussant un nouvel élément

deque, d'autre part, est construit sur une liste doublement liée. Dans une structure de liste chaînée, chaque entrée est stockée dans son propre bloc de mémoire et référence à l'entrée suivante de la liste.

Une liste doublement chaînée est identique, sauf que chaque entrée comporte des références à la fois à l'entrée précédente et à l'entrée suivante de la liste. Cela vous permet d'ajouter facilement des nœuds à l'une des extrémités de la liste.

Pour ajouter une nouvelle entrée dans une structure de liste chaînée, il suffit de définir la référence de la nouvelle entrée afin qu'elle pointe vers le haut de la pile, puis de pointer le haut de la pile vers la nouvelle entrée:

Structure de la mémoire d'une deque poussant un nouvel élément

Cet ajout et la suppression à temps constant d'entrées sur une pile s'accompagne toutefois d'un compromis. Obtenir myDeque[3] est plus lent que pour une liste, car Python doit parcourir chaque nœud de la liste pour accéder au troisième élément.

Heureusement, il est rare que vous souhaitiez effectuer une indexation ou une découpe aléatoire sur une pile. La plupart des opérations sur une pile sont soit pousser ou pop.

Le temps constant .ajouter() et .pop() les opérations font deque un excellent choix pour implémenter une pile Python si votre code n’utilise pas de threading.

Piles de Python et Filetage

Les piles Python peuvent également être utiles dans les programmes multithreads, mais si le filetage ne vous intéresse pas, vous pouvez en toute sécurité ignorer cette section et accéder au résumé.

Les deux options que vous avez vues jusqu'à présent, liste et deque, se comporte différemment si votre programme a des threads.

Pour commencer par le plus simple, vous ne devriez jamais utiliser liste pour toute structure de données accessible par plusieurs threads. liste n'est pas thread-safe.

deque est un peu plus complexe, cependant. Si vous lisez la documentation pour deque, il est clairement indiqué que le .ajouter() et .pop() les opérations sont atomiques, ce qui signifie qu’elles ne seront pas interrompues par un autre thread.

Donc, si vous vous limitez à utiliser uniquement .ajouter() et .pop(), alors vous serez en sécurité.

Le souci d'utiliser deque dans un environnement threadé, il y a d'autres méthodes dans cette classe, et celles-ci ne sont pas spécifiquement conçues pour être atomiques, et elles ne sont pas thread-safe.

Ainsi, s’il est possible de construire une pile Python thread-safe en utilisant un dequeEn agissant de la sorte, vous vous exposez à une utilisation abusive de celle-ci à l’avenir et à des conditions difficiles.

Bon, si vous filez, vous ne pouvez pas utiliser liste pour une pile et vous ne voulez probablement pas utiliser deque pour une pile, alors comment pouvez vous construisez une pile Python pour un programme threadé?

La réponse est dans le queue module, queue.LifoQueue. Rappelez-vous comment vous avez appris que les piles fonctionnent sur le principe du dernier entré / premier sorti. Eh bien, c’est ce que la partie «Lifo» de LifoQueue représente.

Alors que l'interface pour liste et deque étaient similaires, LifoQueue les usages .mettre() et .obtenir() pour ajouter et supprimer des données de la pile:

>>>

>>> de queue importation LifoQueue
>>> ma pile = LifoQueue()

>>> ma pile.mettre('une')
>>> ma pile.mettre('b')
>>> ma pile.mettre('c')

>>> ma pile


>>> ma pile.obtenir()
'c'
>>> ma pile.obtenir()
'b'
>>> ma pile.obtenir()
'une'

>>> # myStack.get () <--- attend pour toujours
>>> ma pile.get_nowait()
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "/usr/lib/python3.7/queue.py", ligne 198, dans get_nowait
    revenir soi.obtenir(bloc=Faux)
  Fichier "/usr/lib/python3.7/queue.py", ligne 167, dans obtenir
    élever Vide
_queue.Empty

contrairement à deque, LifoQueue est conçu pour être entièrement thread-safe. Toutes ses méthodes peuvent être utilisées en toute sécurité dans un environnement threadé. Il ajoute également des délais d'expiration optionnels à ses opérations, ce qui peut souvent constituer une fonctionnalité indispensable dans les programmes threadés.

Cette sécurité de filetage complet a cependant un coût. Pour atteindre ce fil-sécurité, LifoQueue doit faire un peu plus de travail à chaque opération, ce qui signifie que cela prendra un peu plus longtemps.

Fréquemment, ce léger ralentissement n’affectera pas la vitesse globale de votre programme, mais si vous avez mesuré vos performances et découvert que vos opérations de pile sont le goulot d’étranglement, passez ensuite à un deque pourrait valoir la peine d'être fait.

Je voudrais souligner de nouveau que le passage de LifoQueue à deque Parce que c’est plus rapide sans que les mesures montrent que vos opérations de pile sont un goulot d’étranglement, c’est un exemple d’optimisation prématurée. Ne fais pas ça.

Python Stacks: Quelle implémentation devriez-vous utiliser?

En général, vous devriez utiliser un deque si vous n’utilisez pas de threading. Si vous utilisez le threading, vous devriez utiliser un LifoQueue à moins que vous n'ayez mesuré vos performances et constaté qu'une légère augmentation de la vitesse de poussée et de sautillement fera suffisamment de différence pour garantir les risques liés à la maintenance.

liste peut être familier, mais cela devrait être évité car il peut potentiellement avoir des problèmes de réallocation de la mémoire. Les interfaces pour deque et liste sont identiques, et deque n’a pas ces problèmes, ce qui rend deque le meilleur choix pour votre pile Python non-threadée.

Conclusion

Vous savez maintenant ce qu'est une pile et vous avez vu des situations où elles peuvent être utilisées dans des programmes réels. Vous avez évalué trois options différentes pour implémenter des piles et constaté que deque est un excellent choix pour les programmes non-threadés. Si vous implémentez une pile dans un environnement de thread, c’est probablement une bonne idée d’utiliser un LifoQueue.

Vous êtes maintenant capable de:

  • Reconnaître quand une pile serait une bonne structure de données
  • Sélectionnez quelle implémentation convient le mieux à votre problème

Si vous avez encore des questions, n'hésitez pas à vous adresser aux commentaires ci-dessous. Maintenant, allez écrire du code puisque vous avez acquis un autre outil pour vous aider à résoudre vos problèmes de programmation!