Trois façons de surveiller votre code – Real Python

By | décembre 30, 2019

trouver un expert Python

Alors que de nombreux développeurs reconnaissent Python comme un langage de programmation efficace, les programmes Python purs peuvent s'exécuter plus lentement que leurs homologues dans les langages compilés comme C, Rust et Java. Tout au long de ce didacticiel, vous verrez comment utiliser un Minuterie Python pour surveiller la vitesse d'exécution de vos programmes.

Dans ce didacticiel, vous allez apprendre à utiliser:

  • time.perf_counter () mesurer le temps en Python
  • Des classes garder l'état
  • Gestionnaires de contexte travailler avec un bloc de code
  • Décorateurs personnaliser une fonction

Vous acquerrez également des connaissances de base sur le fonctionnement des classes, des gestionnaires de contexte et des décorateurs. Comme vous voyez des exemples de chaque concept, vous serez inspiré d'utiliser un ou plusieurs d'entre eux dans votre code, à la fois pour l'exécution du code de synchronisation et d'autres applications. Chaque méthode a ses avantages et vous apprendrez laquelle utiliser en fonction de la situation. De plus, vous disposerez d'une minuterie Python fonctionnelle que vous pourrez utiliser pour surveiller vos programmes!

Minuteurs Python

Tout d'abord, vous examinerez un exemple de code que vous utiliserez tout au long du didacticiel. Plus tard, vous ajouterez un Minuterie Python à ce code pour surveiller ses performances. Vous verrez également certaines des façons les plus simples de mesurer le temps d'exécution de cet exemple.

Fonctions de minuterie Python

Si vous regardez le construit temps module en Python, alors vous remarquerez plusieurs fonctions qui peuvent mesurer le temps:

Python 3.7 a introduit plusieurs nouvelles fonctions, comme thread_time (), aussi bien que nanoseconde versions de toutes les fonctions ci-dessus, nommées avec un _ns suffixe. Par exemple, perf_counter_ns () est la version nanoseconde de perf_counter (). Vous en apprendrez plus sur ces fonctions plus tard. Pour l'instant, notez ce que la documentation a à dire sur perf_counter ():

Renvoie la valeur (en fraction de seconde) d'un compteur de performances, c'est-à-dire une horloge avec la résolution disponible la plus élevée pour mesurer une courte durée. (La source)

Tout d'abord, vous utiliserez perf_counter () pour créer une minuterie Python. Plus tard, vous allez comparer cela avec d'autres fonctions de minuterie Python et découvrir pourquoi perf_counter () est généralement le meilleur choix.

Exemple: télécharger des didacticiels

Pour mieux comparer les différentes façons d'ajouter un minuteur Python à votre code, vous appliquerez différentes fonctions de minuteur Python au même exemple de code tout au long de ce didacticiel. Si vous avez déjà du code que vous souhaitez mesurer, n'hésitez pas à suivre les exemples à la place.

L'exemple que vous verrez dans ce didacticiel est une fonction courte qui utilise le lecteur realpython package pour télécharger les derniers tutoriels disponibles ici sur Vrai Python. Pour en savoir plus sur Real Python Reader et son fonctionnement, consultez Comment publier un package Python Open-Source sur PyPI. Vous pouvez installer lecteur realpython sur votre système avec pépin:

$ python -m pip installe realpython-reader

Ensuite, vous pouvez importer le package en tant que lecteur.

Vous stockerez l'exemple dans un fichier nommé latest_tutorial.py. Le code se compose d'une fonction qui télécharge et imprime le dernier didacticiel de Vrai Python:

    1 # latest_tutorial.py
    2 
    3 de lecteur importation alimentation
    4 
    5 def principale():
    6     "" "Téléchargez et imprimez le dernier didacticiel de Real Python" ""
    sept     Didacticiel = alimentation.get_article(0)
    8     impression(Didacticiel)
    9 
dix si __Nom__ == "__principale__":
11     principale()

lecteur realpython gère la plupart du travail acharné:

  • Ligne 3 importations alimentation de lecteur realpython. Ce module contient des fonctionnalités pour télécharger des didacticiels à partir du Vrai Python alimentation.
  • Ligne 7 télécharge le dernier tutoriel depuis Vrai Python. Le nombre 0 est un décalage, où 0 signifie le didacticiel le plus récent, 1 est le didacticiel précédent, etc.
  • Ligne 8 imprime le didacticiel sur la console.
  • Ligne 11 appels principale() lorsque vous exécutez le script.

Lorsque vous exécutez cet exemple, votre sortie ressemble généralement à ceci:

$ python latest_tutorial.py
# Fonctions de minuterie Python: trois façons de surveiller votre code

Alors que de nombreux développeurs reconnaissent Python comme un langage de programmation efficace,
les programmes Python purs peuvent s'exécuter plus lentement que leurs homologues compilés
des langages comme C, Rust et Java. Tout au long de ce didacticiel, vous verrez comment
utilisez une minuterie Python pour surveiller la vitesse d'exécution de vos programmes.

[ ... The full text of the tutorial ... ]

Le code peut prendre un peu de temps à s'exécuter en fonction du réseau, vous pouvez donc utiliser un minuteur Python pour surveiller les performances du script.

Votre première minuterie Python

Ajoutons un minuteur Python simple à l'exemple avec time.perf_counter (). Encore une fois, c'est un compteur de performance qui est bien adapté pour chronométrer des parties de votre code.

perf_counter () mesure le temps en secondes à partir d'un moment dans le temps non spécifié, ce qui signifie que la valeur de retour d'un seul appel à la fonction n'est pas utile. Cependant, lorsque vous regardez la différence entre deux appels à perf_counter (), vous pouvez déterminer combien de secondes se sont écoulées entre les deux appels:

>>>

>>> importation temps
>>> temps.perf_counter()
32311.48899951

>>> temps.perf_counter()  # Quelques secondes plus tard
32315.261320793

Dans cet exemple, vous avez appelé deux fois perf_counter () près de 4 secondes d'intervalle. Vous pouvez le confirmer en calculant la différence entre les deux sorties: 32315,26 – 32311,49 = 3,77.

Vous pouvez maintenant ajouter une minuterie Python à l'exemple de code:

    1 # latest_tutorial.py
    2 
    3 importation temps
    4 de lecteur importation alimentation
    5 
    6 def principale():
    sept     "" "Imprimer le dernier didacticiel de Real Python" ""
    8     tic = temps.perf_counter()
    9     Didacticiel = alimentation.get_article(0)
dix     toc = temps.perf_counter()
11     impression(F"Téléchargé le tutoriel en toc - tic: 0.4f secondes")
12 
13     impression(Didacticiel)
14 
15 si __Nom__ == "__principale__":
16     principale()

Notez que vous appelez perf_counter () avant et après le téléchargement du didacticiel. Vous imprimez ensuite le temps nécessaire au téléchargement du didacticiel en calculant la différence entre les deux appels.

Maintenant, lorsque vous exécutez l'exemple, vous verrez le temps écoulé avant le didacticiel:

$ python latest_tutorial.py
Téléchargé le tutoriel en 0,67 secondes
# Fonctions de minuterie Python: trois façons de surveiller votre code

[ ... The full text of the tutorial ... ]

C'est ça! Vous avez abordé les bases du chronométrage de votre propre code Python. Dans le reste du didacticiel, vous apprendrez comment encapsuler un minuteur Python dans une classe, un gestionnaire de contexte et un décorateur pour le rendre plus cohérent et plus pratique à utiliser.

Une classe de minuterie Python

Revoyez comment vous avez ajouté le minuteur Python à l'exemple ci-dessus. Notez que vous avez besoin d'au moins une variable (tic) pour stocker l'état du minuteur Python avant de télécharger le didacticiel. Après avoir regardé un peu le code, vous remarquerez peut-être également que les trois lignes en surbrillance sont ajoutées uniquement à des fins de synchronisation! Maintenant, vous allez créer une classe qui fait la même chose que vos appels manuels à perf_counter (), mais d'une manière plus lisible et cohérente.

Tout au long de ce didacticiel, vous allez créer et mettre à jour Minuteur, une classe que vous pouvez utiliser pour chronométrer votre code de différentes manières. Le code final est également disponible sur PyPI sous le nom codetiming. Vous pouvez l'installer sur votre système comme ceci:

$ python -m pip install codetiming

Vous pouvez trouver plus d'informations sur codetiming plus loin dans ce didacticiel, dans la section intitulée The Python Timer Code.

Comprendre les classes en Python

Des classes sont les principaux éléments constitutifs de la programmation orientée objet. UNE classe est essentiellement un modèle que vous pouvez utiliser pour créer objets. Bien que Python ne vous oblige pas à programmer de manière orientée objet, les classes sont partout dans le langage. Pour une preuve rapide, examinons les temps module:

>>>

>>> importation temps
>>> type(temps)


>>> temps.__classe__

type() renvoie le type d'un objet. Ici, vous pouvez voir que les modules sont en fait des objets créés à partir d'un module classe. L'attribut spécial .__classe__ peut être utilisé pour accéder à la classe qui définit un objet. En fait, presque tout en Python est une classe:

>>>

>>> type(3)


>>> type(Aucun)


>>> type(impression)


>>> type(type)

En Python, les classes sont excellentes lorsque vous devez modéliser quelque chose qui doit garder la trace d'un état particulier. En général, une classe est une collection de propriétés (appelée les attributs) et les comportements (appelés les méthodes). Pour plus d'informations sur les classes et la programmation orientée objet, consultez la programmation orientée objet (OOP) en Python 3 ou les documents officiels.

Création d'une classe de minuterie Python

Les cours sont bons pour le suivi Etat. Dans un Minuteur vous voulez savoir quand un chronomètre démarre et combien de temps s'est écoulé depuis. Pour la première mise en œuvre de Minuteur, vous allez ajouter un ._Heure de début attribut, ainsi que .début() et .Arrêtez() méthodes. Ajoutez le code suivant à un fichier nommé timer.py:

    1 # timer.py
    2 
    3 importation temps
    4 
    5 classe TimerError(Exception):
    6     "" "Une exception personnalisée utilisée pour signaler les erreurs d'utilisation de la classe Timer" ""
    sept 
    8 classe Minuteur:
    9     def __init__(soi):
dix         soi._Heure de début = Aucun
11 
12     def début(soi):
13         "" "Démarrer une nouvelle minuterie" ""
14         si soi._Heure de début est ne pas Aucun:
15             élever TimerError(F"La minuterie est en cours d'exécution. Utilisez .stop () pour l'arrêter")
16 
17         soi._Heure de début = temps.perf_counter()
18 
19     def Arrêtez(soi):
20         "" "Arrêtez la minuterie et signalez le temps écoulé" ""
21         si soi._Heure de début est Aucun:
22             élever TimerError(F"Le minuteur ne fonctionne pas. Utilisez .start () pour le démarrer")
23 
24         temps écoulé = temps.perf_counter() - soi._Heure de début
25         soi._Heure de début = Aucun
26         impression(F"Temps écoulé: elapsed_time: 0.4f    secondes ")

Plusieurs choses se produisent ici, alors parcourons le code étape par étape.

À la ligne 5, vous définissez un TimerError classe. le (Exception) la notation signifie que TimerError hérite d'une autre classe appelée Exception. Python utilise cette classe intégrée pour la gestion des erreurs. Vous n'avez pas besoin d'ajouter d'attributs ou de méthodes à TimerError. Cependant, avoir une erreur personnalisée vous donnera plus de flexibilité pour gérer les problèmes à l'intérieur Minuteur. Pour plus d'informations, consultez Exceptions Python: une introduction.

La définition de Minuteur lui-même commence à la ligne 8. Lorsque vous créez ou instancier un objet d'une classe, votre code appelle la méthode spéciale .__ init __ (). Dans cette première version de Minuteur, vous initialisez uniquement le ._Heure de début , que vous utiliserez pour suivre l'état de votre minuterie Python. Il a la valeur Aucun lorsque la minuterie ne fonctionne pas. Une fois le chronomètre en marche, ._Heure de début garde une trace du démarrage de la minuterie.

Quand vous appelez .début() pour démarrer un nouveau minuteur Python, vous devez d'abord vérifier que le minuteur n'est pas déjà en cours d'exécution. Ensuite, vous stockez la valeur actuelle de perf_counter () dans ._Heure de début. D'un autre côté, lorsque vous appelez .Arrêtez(), vous vérifiez d'abord que le minuteur Python est en cours d'exécution. Si tel est le cas, vous calculez le temps écoulé comme la différence entre la valeur actuelle de perf_counter () et celui dans lequel vous avez stocké ._Heure de début. Enfin, vous réinitialisez ._Heure de début afin que la minuterie puisse être redémarrée et imprimer le temps écoulé.

Voici comment vous utilisez Minuteur:

>>>

>>> de minuteur importation Minuteur
>>> t = Minuteur()
>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
Temps écoulé: 3,8191 secondes

Comparez cela à l'exemple précédent où vous avez utilisé perf_counter () directement. La structure du code est assez similaire, mais maintenant le code est plus clair, et c'est l'un des avantages de l'utilisation des classes. En choisissant soigneusement vos noms de classe, de méthode et d'attribut, vous pouvez rendre votre code très descriptif!

Utilisation de la classe de minuterie Python

Appliquons Minuteur à latest_tutorial.py. Vous n'avez qu'à apporter quelques modifications à votre code précédent:

# latest_tutorial.py

de minuteur importation Minuteur
de lecteur importation alimentation

def principale():
    "" "Imprimer le dernier didacticiel de Real Python" ""
    t = Minuteur()
    t.début()
    Didacticiel = alimentation.get_article(0)
    t.Arrêtez()

    impression(Didacticiel)

si __Nom__ == "__principale__":
    principale()

Notez que le code est très similaire à ce que vous avez vu précédemment. En plus de rendre le code plus lisible, Minuteur prend en charge l'impression du temps écoulé sur la console, ce qui rend la consignation du temps passé plus cohérente. Lorsque vous exécutez le code, vous verrez à peu près la même sortie:

$ python latest_tutorial.py
Temps écoulé: 0,64 secondes
# Fonctions de minuterie Python: trois façons de surveiller votre code

[ ... The full text of the tutorial ... ]

Impression du temps écoulé depuis Minuteur peut être cohérent, mais il semble que cette approche ne soit pas très flexible. Dans la section suivante, vous verrez comment personnaliser votre classe.

Plus de commodité et de flexibilité

Jusqu'à présent, vous avez vu que les classes conviennent lorsque vous souhaitez encapsuler un état et garantir un comportement cohérent dans votre code. Dans cette section, vous ajouterez plus de commodité et de flexibilité à votre minuterie Python:

  • Utilisation texte et mise en forme adaptables lors du rapport du temps passé
  • Appliquer journalisation flexible, à l'écran, dans un fichier journal ou dans d'autres parties de votre programme
  • Créer une minuterie Python qui peut s'accumuler sur plusieurs invocations
  • Construire une représentation informative d'un temporisateur Python

Voyons d'abord comment personnaliser le texte utilisé pour signaler le temps passé. Dans le code précédent, le texte f "Temps écoulé: elapsed_time: 0.4f secondes" est codé en dur dans .Arrêtez(). Vous pouvez ajouter de la flexibilité aux classes en utilisant variables d'instance. Leurs valeurs sont normalement passées comme arguments à .__ init __ () et stocké sous soi les attributs. Pour plus de commodité, vous pouvez également fournir des valeurs par défaut raisonnables.

Ajouter .texte comme un Minuteur variable d'instance, vous allez faire quelque chose comme ceci:

def __init__(soi, texte="Temps écoulé: : 0.4f    secondes "):
    soi._Heure de début = Aucun
    soi.texte = texte

Notez que le texte par défaut, "Temps écoulé: : 0.4f secondes", est donné sous forme de chaîne régulière et non sous forme de f-chaîne. Vous ne pouvez pas utiliser de chaîne f ici car ils évaluent immédiatement et lorsque vous instanciez Minuteur, votre code n'a pas encore calculé le temps écoulé.

Dans .Arrêtez(), tu utilises .texte comme modèle et .format() pour remplir le modèle:

def Arrêtez(soi):
    "" "Arrêtez la minuterie et signalez le temps écoulé" ""
    si soi._Heure de début est Aucun:
        élever TimerError(F"Le minuteur ne fonctionne pas. Utilisez .start () pour le démarrer")

    temps écoulé = temps.perf_counter() - soi._Heure de début
    soi._Heure de début = Aucun
    impression(soi.texte.format(temps écoulé))

Après cette mise à jour vers timer.py, vous pouvez modifier le texte comme suit:

>>>

>>> de minuteur importation Minuteur
>>> t = Minuteur(texte="Tu as attendu : .1f    secondes ")
>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
Vous avez attendu 4,1 secondes

Supposez ensuite que vous ne souhaitez pas simplement imprimer un message sur la console. Vous souhaitez peut-être enregistrer vos mesures de temps afin de pouvoir les stocker dans une base de données. Vous pouvez le faire en renvoyant la valeur de temps écoulé de .Arrêtez(). Ensuite, le code appelant peut choisir d'ignorer cette valeur de retour ou de l'enregistrer pour un traitement ultérieur.

Vous souhaitez peut-être intégrer Minuteur dans vos routines de journalisation. Pour prendre en charge la journalisation ou d'autres sorties de Minuteur vous devez changer l'appel impression() afin que l'utilisateur puisse fournir sa propre fonction de journalisation. Cela peut être fait de la même manière que vous avez personnalisé le texte précédemment:

def __init__(soi, texte="Temps écoulé: : 0.4f    secondes ", enregistreur=impression):
    soi._Heure de début = Aucun
    soi.texte = texte
    soi.enregistreur = enregistreur

def Arrêtez(soi):
    "" "Arrêtez la minuterie et signalez le temps écoulé" ""
    si soi._Heure de début est Aucun:
        élever TimerError(F"Le minuteur ne fonctionne pas. Utilisez .start () pour le démarrer")

    temps écoulé = temps.perf_counter() - soi._Heure de début
    soi._Heure de début = Aucun

    si soi.enregistreur:
        soi.enregistreur(soi.texte.format(temps écoulé))

    revenir temps écoulé

À la place d'utiliser impression() directement, vous créez une autre variable d'instance, self.logger, cela devrait faire référence à une fonction qui prend une chaîne en argument. En plus de impression(), vous pouvez utiliser des fonctions comme logging.info () ou .écrire() sur les objets fichier. Notez également le si test, qui vous permet de désactiver complètement l'impression en passant enregistreur = Aucun.

Voici deux exemples qui montrent la nouvelle fonctionnalité en action:

>>>

>>> de minuteur importation Minuteur
>>> importation enregistrement
>>> t = Minuteur(enregistreur=enregistrement.avertissement)
>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
AVERTISSEMENT: root: Temps écoulé: 3,1610 secondes
3.1609658249999484

>>> t = Minuteur(enregistreur=Aucun)
>>> t.début()

>>> valeur = t.Arrêtez()  # Quelques secondes plus tard
>>> valeur
4.710851433001153

Lorsque vous exécutez ces exemples dans un shell interactif, Python imprime automatiquement la valeur de retour.

La troisième amélioration que vous ajouterez est la possibilité d'accumuler mesures du temps. Vous voudrez peut-être le faire, par exemple, lorsque vous appelez une fonction lente dans une boucle. Vous allez ajouter un peu plus de fonctionnalités sous la forme de temporisateurs nommés avec un dictionnaire qui garde la trace de chaque temporisateur Python dans votre code.

Supposons que vous étendez latest_tutorial.py à un latest_tutorials.py script qui télécharge et imprime les dix derniers didacticiels de Vrai Python. Voici une mise en œuvre possible:

# latest_tutorials.py

de minuteur importation Minuteur
de lecteur importation alimentation

def principale():
    "" "Imprimez les 10 derniers didacticiels de Real Python" ""
    t = Minuteur(texte="10 tutoriels téléchargés en : 0.2f    secondes ")
    t.début()
    pour tutorial_num dans intervalle(dix):
        Didacticiel = alimentation.get_article(tutorial_num)
        impression(Didacticiel)
    t.Arrêtez()

si __Nom__ == "__principale__":
    principale()

Le code boucle sur les nombres de 0 à 9 et les utilise comme arguments de décalage pour feed.get_article (). Lorsque vous exécutez le script, vous verrez de nombreuses informations imprimées sur votre console:

$ python latest_tutorials.py
# Fonctions de minuterie Python: trois façons de surveiller votre code

[ ... The full text of ten tutorials ... ]
10 tutoriels téléchargés en 0,67 seconde

Un problème subtil avec ce code est que vous mesurez non seulement le temps nécessaire pour télécharger les didacticiels, mais également le temps que Python passe à imprimer les didacticiels sur votre écran. Cela pourrait ne pas être si important car le temps passé à imprimer devrait être négligeable par rapport au temps passé à télécharger. Pourtant, il serait bon d'avoir un moyen de chronométrer précisément ce que vous recherchez dans ce genre de situations.

Il existe plusieurs façons de contourner ce problème sans modifier l'implémentation actuelle de Minuteur. Cependant, la prise en charge de ce cas d'utilisation sera très utile et peut être effectuée avec seulement quelques lignes de code.

Tout d'abord, vous allez introduire un dictionnaire appelé .timers comme un variable de classe sur Minuteur, ce qui signifie que toutes les instances de Minuteur le partagera. Vous l'implémentez en le définissant en dehors de toute méthode:

classe Minuteur:
    minuteries = dicter()

Les variables de classe sont accessibles soit directement sur la classe, soit via une instance de la classe:

>>>

>>> de minuteur importation Minuteur
>>> Minuteur.minuteries


>>> t = Minuteur()
>>> t.minuteries


>>> Minuteur.minuteries est t.minuteries
Vrai

Dans les deux cas, le code renvoie le même dictionnaire de classe vide.

Ensuite, vous ajouterez des noms facultatifs à votre minuterie Python. Vous pouvez utiliser le nom à deux fins différentes:

  1. En levant le temps écoulé plus tard dans votre code
  2. Accumuler minuteries du même nom

Pour ajouter des noms à votre minuterie Python, vous devez apporter deux modifications supplémentaires à timer.py. Premier, Minuteur devrait accepter le Nom comme paramètre. Deuxièmement, le temps écoulé devrait être ajouté à .timers quand une minuterie s'arrête:

classe Minuteur:
    minuteries = dicter()

    def __init__(
        soi,
        Nom=Aucun,
        texte="Temps écoulé: : 0.4f    secondes ",
        enregistreur=impression,
    ):
        soi._Heure de début = Aucun
        soi.Nom = Nom
        soi.texte = texte
        soi.enregistreur = enregistreur

        # Ajouter de nouveaux temporisateurs nommés au dictionnaire des temporisateurs
        si Nom:
            soi.minuteries.définir par defaut(Nom, 0)

    # Les autres méthodes sont inchangées

    def Arrêtez(soi):
        "" "Arrêtez la minuterie et signalez le temps écoulé" ""
        si soi._Heure de début est Aucun:
            élever TimerError(F"Le minuteur ne fonctionne pas. Utilisez .start () pour le démarrer")

        temps écoulé = temps.perf_counter() - soi._Heure de début
        soi._Heure de début = Aucun

        si soi.enregistreur:
            soi.enregistreur(soi.texte.format(temps écoulé))
        si soi.Nom:
            soi.minuteries[[[[soi.Nom] + = temps écoulé

        revenir temps écoulé

Notez que vous utilisez .définir par defaut() lors de l'ajout du nouveau minuteur Python à .timers. Ceci est une excellente fonctionnalité qui ne définit la valeur que si Nom n'est pas déjà défini dans le dictionnaire. Si Nom est déjà utilisé dans .timers, la valeur reste inchangée. Cela vous permet d'accumuler plusieurs minuteries:

>>>

>>> de minuteur importation Minuteur
>>> t = Minuteur("accumuler")
>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
Temps écoulé: 3,7036 secondes
3.703554293999332

>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
Temps écoulé: 2,3449 secondes
2.3448921170001995

>>> Minuteur.minuteries
'accumuler': 6.0484464109995315

Vous pouvez maintenant revoir latest_tutorials.py et assurez-vous que seul le temps consacré au téléchargement des didacticiels est mesuré:

# latest_tutorials.py

de minuteur importation Minuteur
de lecteur importation alimentation

def principale():
    "" "Imprimez les 10 derniers didacticiels de Real Python" ""
    t = Minuteur("Télécharger", enregistreur=Aucun)
    pour tutorial_num dans intervalle(dix):
        t.début()
        Didacticiel = alimentation.get_article(tutorial_num)
        t.Arrêtez()
        impression(Didacticiel)

    download_time = Minuteur.minuteries[[[["Télécharger"]
    impression(F"10 tutoriels téléchargés en download_time: 0.2f    secondes ")

si __Nom__ == "__principale__":
    principale()

La réexécution du script donnera une sortie similaire à celle précédente, bien que vous ne chronométrez maintenant que le téléchargement réel des didacticiels:

$ python latest_tutorials.py
# Fonctions de minuterie Python: trois façons de surveiller votre code

[ ... The full text of ten tutorials ... ]
10 tutoriels téléchargés en 0,65 seconde

La dernière amélioration que vous apporterez à Minuteur est de le rendre plus informatif lorsque vous travaillez avec lui de manière interactive. Essayez ce qui suit:

>>>

>>> de minuteur importation Minuteur
>>> t = Minuteur()
>>> t

Cette dernière ligne est la façon par défaut dont Python représente les objets. Bien que vous puissiez en tirer des informations, elles ne sont généralement pas très utiles. Au lieu de cela, ce serait bien de voir des choses comme le nom du Minuteur, ou comment il rendra compte des horaires.

Dans Python 3.7, des classes de données ont été ajoutées à la bibliothèque standard. Ceux-ci offrent plusieurs commodités à vos classes, y compris une chaîne de représentation plus informative.

Vous convertissez votre minuterie Python en une classe de données à l'aide de la @dataclass décorateur. Vous en apprendrez plus sur les décorateurs plus loin dans ce didacticiel. Pour l'instant, vous pouvez considérer cela comme une notation qui indique à Python que Minuteur est une classe de données:

    1 de classes de données importation classe de données, champ
    2 de dactylographie importation Tout, ClassVar
    3 
    4 @dataclass
    5 classe Minuteur:
    6     minuteries: ClassVar = dicter()
    sept     Nom: Tout = Aucun
    8     texte: Tout = "Temps écoulé: : 0.4f    secondes "
    9     enregistreur: Tout = impression
dix     _Heure de début: Tout = champ(défaut=Aucun, init=Faux, repr=Faux)
11 
12     def __post_init__(soi):
13         "" "Initialisation: ajouter une minuterie au dict des minuteries" ""
14         si soi.Nom:
15             soi.minuteries.définir par defaut(soi.Nom, 0)
16 
17     # Le reste du code est inchangé

Ce code remplace votre ancien .__ init __ () méthode. Notez comment les classes de données utilisent une syntaxe qui ressemble à la syntaxe des variables de classe que vous avez vue précédemment pour définir toutes les variables. En réalité, .__ init __ () est créé automatiquement pour les classes de données, sur la base de variables annotées dans la définition de la classe.

Vous devez annoter vos variables pour utiliser une classe de données. Vous pouvez l'utiliser pour ajouter des indications de type à votre code. Si vous ne souhaitez pas utiliser d'indices de type, vous pouvez annoter toutes les variables avec Tout, comme vous l'avez fait ci-dessus. Vous verrez bientôt comment ajouter des conseils de type réels à votre classe de données.

Voici quelques notes sur le Minuteur classe de données:

  • Ligne 4: le @dataclass décorateur définit Minuteur être une classe de données.

  • Ligne 6: Le spécial ClassVar une annotation est nécessaire pour que les classes de données spécifient que .timers est une variable de classe.

  • Lignes 7 à 9: .Nom, .texte, et .logger sera défini comme des attributs sur Minuteur, dont les valeurs peuvent être spécifiées lors de la création Minuteur instances. Ils ont tous les valeurs par défaut données.

  • Ligne 10: Rappeler que ._Heure de début est un attribut spécial utilisé pour garder une trace de l'état du temporisateur Python, mais qui doit être caché à l'utilisateur. En utilisant dataclasses.field () vous dites que ._Heure de début devrait être retiré de .__ init __ () et la représentation de Minuteur.

  • Lignes 12 à 15: Vous pouvez utiliser le spécial .__ post_init __ () méthode pour toute initialisation que vous devez faire en dehors de la définition des attributs d'instance. Ici, vous l'utilisez pour ajouter des minuteries nommées à .timers.

Votre nouveau Minuteur La classe de données fonctionne exactement comme votre classe régulière précédente, sauf qu'elle a maintenant une belle représentation:

>>>

>>> de minuteur importation Minuteur
>>> t = Minuteur()
>>> t
Timer (name = None, text = 'Elapsed time: : 0.4f seconds',
                        logger =)

>>> t.début()

>>> t.Arrêtez()  # Quelques secondes plus tard
Temps écoulé: 6,7197 secondes
6.719705373998295

Maintenant, vous avez une version assez soignée de Minuteur c'est cohérent, flexible, pratique et instructif! De nombreuses améliorations que vous avez vues dans cette section peuvent également être appliquées à d'autres types de classes dans vos projets.

Avant de terminer cette section, examinons le code source complet de Minuteur dans sa forme actuelle. Vous remarquerez l'ajout d'indices de type au code pour une documentation supplémentaire:

# timer.py

de classes de données importation classe de données, champ
importation temps
de dactylographie importation Callable, ClassVar, Dict, Optionnel

classe TimerError(Exception):
    "" "Une exception personnalisée utilisée pour signaler les erreurs d'utilisation de la classe Timer" ""

@dataclass
classe Minuteur:
    minuteries: ClassVar[[[[Dict[[[[str, flotte]] = dicter()
    Nom: Optionnel[[[[str] = Aucun
    texte: str = "Temps écoulé: : 0.4f    secondes "
    enregistreur: Optionnel[[[[Callable[[[[[[[[str], Aucun]] = impression
    _Heure de début: Optionnel[[[[flotte] = champ(défaut=Aucun, init=Faux, repr=Faux)

    def __post_init__(soi) -> Aucun:
        "" "Ajouter un minuteur pour dicter les minuteurs après l'initialisation" ""
        si soi.Nom est ne pas Aucun:
            soi.minuteries.définir par defaut(soi.Nom, 0)

    def début(soi) -> Aucun:
        "" "Démarrer une nouvelle minuterie" ""
        si soi._Heure de début est ne pas Aucun:
            élever TimerError(F"La minuterie est en cours d'exécution. Utilisez .stop () pour l'arrêter")

        soi._Heure de début = temps.perf_counter()

    def Arrêtez(soi) -> flotte:
        "" "Arrêtez la minuterie et signalez le temps écoulé" ""
        si soi._Heure de début est Aucun:
            élever TimerError(F"Le minuteur ne fonctionne pas. Utilisez .start () pour le démarrer")

        # Calculer le temps écoulé
        temps écoulé = temps.perf_counter() - soi._Heure de début
        soi._Heure de début = Aucun

        # Rapport du temps écoulé
        si soi.enregistreur:
            soi.enregistreur(soi.texte.format(temps écoulé))
        si soi.Nom:
            soi.minuteries[[[[soi.Nom] + = temps écoulé

        revenir temps écoulé

L'utilisation d'une classe pour créer un minuteur Python présente plusieurs avantages:

  • Lisibilité: Votre code se lira plus naturellement si vous choisissez soigneusement les noms de classe et de méthode.
  • Cohérence: Votre code sera plus facile à utiliser si vous encapsulez des propriétés et des comportements dans des attributs et des méthodes.
  • La flexibilité: Votre code sera réutilisable si vous utilisez des attributs avec des valeurs par défaut au lieu de valeurs codées en dur.

Cette classe est très flexible et vous pouvez l'utiliser dans presque toutes les situations où vous souhaitez surveiller le temps nécessaire à l'exécution du code. Cependant, dans les sections suivantes, vous apprendrez à utiliser les gestionnaires de contexte et les décorateurs, ce qui sera plus pratique pour chronométrer les blocs de code et les fonctions.

Un gestionnaire de contexte de minuterie Python

Votre Python Minuteur la classe a parcouru un long chemin! Comparé au premier temporisateur Python que vous avez créé, votre code est devenu assez puissant. Cependant, il reste encore un peu de code standard pour utiliser votre Minuteur:

  1. Tout d'abord, instanciez la classe.
  2. Appel .début() avant le bloc de code que vous souhaitez chronométrer.
  3. Appel .Arrêtez() après le bloc de code.

Heureusement, Python a une construction unique pour appeler des fonctions avant et après un bloc de code: le gestionnaire de contexte. Dans cette section, vous apprendrez ce que sont les gestionnaires de contexte et comment créer le vôtre. Ensuite, vous verrez comment développer Minuteur afin qu'il puisse également fonctionner en tant que gestionnaire de contexte. Enfin, vous verrez comment utiliser Minuteur en tant que gestionnaire de contexte peut simplifier votre code.

Comprendre les gestionnaires de contexte en Python

Les gestionnaires de contexte font partie de Python depuis longtemps. Ils ont été introduits par PEP 343 en 2005 et mis en œuvre pour la première fois en Python 2.5. Vous pouvez reconnaître les gestionnaires de contexte dans le code en utilisant le avec mot-clé:

avec EXPRESSION comme VARIABLE:
    BLOQUER

EXPRESSION est une expression Python qui renvoie un gestionnaire de contexte. Le gestionnaire de contexte est éventuellement lié au nom VARIABLE. Finalement, BLOQUER est un bloc de code Python normal. Le gestionnaire de contexte garantira que votre programme appelle du code avant BLOQUER et un autre code après BLOQUER s'exécute. Ce dernier se produira, même si BLOQUER lève une exception.

L'utilisation la plus courante des gestionnaires de contexte consiste probablement à gérer différentes ressources, comme les fichiers, les verrous et les connexions à la base de données. Le gestionnaire de contexte est ensuite utilisé pour libérer et nettoyer la ressource après l'avoir utilisée. L'exemple suivant révèle la structure fondamentale de timer.py en imprimant uniquement les lignes contenant deux points. Plus important encore, il montre l'idiome commun pour ouvrir un fichier en Python:

>>>

>>> avec ouvert("timer.py") comme fp:
...     impression("".joindre(ln pour ln dans fp si ":" dans ln))
...
classe TimerError (Exception):
Minuterie de classe:
                minuteries: ClassVar[Dict[str, float]]= dict ()
                nom: (optionnel[str] = Aucun
                text: str = "Temps écoulé: : 0.4f secondes"
                enregistreur: facultatif[Appelable[[Appelable[[Callable[[Callable[[str], Aucun]]= imprimer
                _start_time: facultatif[float] = champ (par défaut = Aucun, init = Faux, repr = Faux)
                def __post_init __ (self) -> Aucun:
                                si self.name n'est pas None:
                def start (self) -> Aucun:
                                si self._start_time n'est pas None:
                def stop (self) -> float:
                                si self._start_time est None:
                                si self.logger:
                                si self.name:

Notez que fp, le pointeur de fichier, n'est jamais explicitement fermé car vous avez utilisé ouvert() en tant que gestionnaire de contexte. Vous pouvez confirmer que fp s'est fermé automatiquement:

Dans cet exemple, open ("timer.py") est une expression qui renvoie un gestionnaire de contexte. Ce gestionnaire de contexte est lié au nom fp. Le gestionnaire de contexte est en vigueur lors de l'exécution de impression(). Ce bloc de code d'une ligne s'exécute dans le contexte de fp.

Qu'est-ce que cela signifie fp est un gestionnaire de contexte? Techniquement, cela signifie que fp met en œuvre le protocole du gestionnaire de contexte. Il existe de nombreux protocoles différents sous-jacents au langage Python. Vous pouvez considérer un protocole comme un contrat qui indique quelles méthodes spécifiques votre code doit implémenter.

Le protocole du gestionnaire de contexte se compose de deux méthodes:

  1. Appel .__entrer__() lors de la saisie du contexte lié au gestionnaire de contexte.
  2. Appel .__sortie__() à la sortie du contexte lié au gestionnaire de contexte.

En d'autres termes, pour créer vous-même un gestionnaire de contexte, vous devez écrire une classe qui implémente .__entrer__() et .__sortie__(). Ni plus ni moins. Essayons un Bonjour le monde! exemple de gestionnaire de contexte:

# greeter.py

classe Greeter:
    def __init__(soi, Nom):
        soi.Nom = Nom

    def __entrer__(soi):
        impression(F"Bonjour self.name")
        revenir soi

    def __sortie__(soi, exc_type, exc_value, exc_tb):
        impression(F"À plus tard, self.name")

Greeter est un gestionnaire de contexte car il implémente le protocole du gestionnaire de contexte. Vous pouvez l'utiliser comme ceci:

>>>

>>> de saluer importation Greeter
>>> avec Greeter("Pseudo"):
...     impression("Faire des trucs ...")
...
Bonjour Nick
Faire des trucs ...
A plus tard, Nick

Tout d'abord, notez comment .__entrer__() est appelé avant de faire des choses, tandis que .__sortie__() est appelé après. Dans cet exemple simplifié, vous ne faites pas référence au gestionnaire de contexte. Dans ce cas, vous n'avez pas besoin de donner au gestionnaire de contexte un nom avec comme.

Ensuite, remarquez comment .__entrer__() Retour soi. La valeur de retour de .__entrer__() est ce qui est lié par comme. Vous souhaitez généralement revenir soi de .__entrer__() lors de la création de gestionnaires de contexte. Vous pouvez utiliser cette valeur de retour comme suit:

>>>

>>> de saluer importation Greeter
>>> avec Greeter("Emilie") comme grt:
...     impression(F"grt.name    fait des trucs ... ")
...
Bonjour Emily
Emily fait des trucs ...
A plus tard, Emily

Finalement, .__sortie__() prend trois arguments: exc_type, exc_value, et exc_tb. Ils sont utilisés pour la gestion des erreurs dans le gestionnaire de contexte et reflètent les valeurs de retour de sys.exc_info (). Si une exception se produit pendant l'exécution du bloc, votre code appelle .__sortie__() avec le type de l'exception, une instance d'exception et un objet traceback. Souvent, vous pouvez les ignorer dans votre gestionnaire de contexte, auquel cas .__sortie__() est appelé avant que l'exception ne soit relancée:

>>>

>>> de saluer importation Greeter
>>> avec Greeter("Coquin") comme grt:
...     impression(F"grt.age    n'existe pas")
...
Bonjour Rascal
A plus tard, Rascal
Traceback (dernier appel le plus récent):
  Fichier "", ligne 2, dans 
AttributeError: L'objet 'Greeter' n'a pas d'attribut 'age'

Tu peux voir ça "A plus tard, Rascal" est imprimé, même s'il y a une erreur dans le code.

Vous savez maintenant ce que sont les gestionnaires de contexte et comment créer le vôtre. Si vous voulez plonger plus profondément, consultez contextlib dans la bibliothèque standard. Il comprend des moyens pratiques pour définir de nouveaux gestionnaires de contexte, ainsi que des gestionnaires de contexte prêts à l'emploi qui peuvent être utilisés pour fermer des objets, supprimer des erreurs ou même ne rien faire! Pour encore plus d'informations, consultez les gestionnaires de contexte Python et la déclaration «with» et le didacticiel qui l'accompagne.

Création d'un gestionnaire de contexte de minuterie Python

Vous avez vu comment les gestionnaires de contexte fonctionnent en général, mais comment peuvent-ils aider avec le code temporel? Si vous pouvez exécuter certaines fonctions avant et après un bloc de code, vous pouvez simplifier le fonctionnement de votre minuterie Python. Jusqu'à présent, vous avez dû appeler .début() et .Arrêtez() explicitement lors du chronométrage de votre code, mais un gestionnaire de contexte peut le faire automatiquement.

Encore une fois, pour Minuteur pour fonctionner en tant que gestionnaire de contexte, il doit respecter le protocole du gestionnaire de contexte. En d'autres termes, il doit mettre en œuvre .__entrer__() et .__sortie__() pour démarrer et arrêter le minuteur Python. Toutes les fonctionnalités nécessaires sont déjà disponibles, il n'y a donc pas beaucoup de nouveau code à écrire. Ajoutez simplement les méthodes suivantes à votre Minuteur classe:

def __entrer__(soi):
    "" "Démarrer une nouvelle minuterie en tant que gestionnaire de contexte" ""
    soi.début()
    revenir self

def __exit__(self, *exc_info):
    """Stop the context manager timer"""
    self.Arrêtez()

Minuteur is now a context manager. The important part of the implementation is that .__enter__() calls .start() to start a Python timer when the context is entered, and .__exit__() uses .stop() to stop the Python timer when the code leaves the context. Try it out:

>>>

>>> de timer import Minuteur
>>> import temps
>>> avec Minuteur():
...     temps.sommeil(0.7)
...
Elapsed time: 0.7012 seconds

You should also note two more subtle details:

  1. .__enter__() Retour self, les Minuteur instance, which allows the user to bind the Minuteur instance to a variable using as. Par exemple, with Timer() as t: will create the variable t pointing to the Minuteur object.

  2. .__exit__() expects a triple of arguments with information about any exception that occurred during the execution of the context. In your code, these arguments are packed into a tuple called exc_info and then ignored, which means that Minuteur will not attempt any exception handling.

.__exit__() doesn’t do any error handling in this case. Still, one of the great features of context managers is that they’re guaranteed to call .__exit__(), no matter how the context exits. In the following example, you purposely create an error by dividing by zero:

>>>

>>> de timer import Minuteur
>>> avec Minuteur():
...     pour num dans intervalle(-3, 3):
...         impression(f"1 / num    = 1 / num:.3f")
...
1 / -3 = -0.333
1 / -2 = -0.500
1 / -1 = -1.000
Elapsed time: 0.0001 seconds
Traceback (most recent call last):
  Fichier "", line 3, in 
ZeroDivisionError: division by zero

Notez que Minuteur prints out the elapsed time, even though the code crashed. It’s possible to inspect and suppress errors in .__exit__(). See the documentation for more information.

Using the Python Timer Context Manager

Let’s see how to use the Minuteur context manager to time the download of Real Python tutorials. Recall how you used Minuteur earlier:

# latest_tutorial.py

de timer import Minuteur
de reader import feed

def principale():
    """Print the latest tutorial from Real Python"""
    t = Minuteur()
    t.début()
    Didacticiel = feed.get_article(0)
    t.Arrêtez()

    impression(Didacticiel)

si __name__ == "__main__":
    principale()

You’re timing the call to feed.get_article(). You can use the context manager to make the code shorter, simpler, and more readable:

# latest_tutorial.py

de timer import Minuteur
de reader import feed

def principale():
    """Print the latest tutorial from Real Python"""
    avec Minuteur():
        Didacticiel = feed.get_article(0)

    impression(Didacticiel)

si __name__ == "__main__":
    principale()

This code does virtually the same as the code above. The main difference is that you don’t define the extraneous variable t, which keeps your namespace cleaner.

Running the script should give a familiar result:

$ python latest_tutorial.py
Elapsed time: 0.71 seconds
# Python Timer Functions: Three Ways to Monitor Your Code

[ ... The full text of the tutorial ... ]

There are a few advantages to adding context manager capabilities to your Python timer class:

  • Low effort: You only need one extra line of code to time the execution of a block of code.
  • Readability: Invoking the context manager is readable, and you can more clearly visualize the code block you’re timing.

En utilisant Minuteur as a context manager is almost as flexible as using .start() et .stop() directly, while it has less boilerplate code. In the next section, you’ll see how Minuteur can be used as a decorator as well. This will make it easier to monitor the runtime of complete functions.

A Python Timer Decorator

Votre Minuteur class is now very versatile. However, there’s one use case where it could be even more streamlined. Say that you want to track the time spent inside one given function in your codebase. Using a context manager, you have essentially two different options:

  1. Utilisation Minuteur every time you call the function:

    avec Minuteur("some_name"):
        do_something()
    

    If you call do_something() in many places, then this will become cumbersome and hard to maintain.

  2. Wrap the code in your function inside a context manager:

    def do_something():
        avec Minuteur("some_name"):
            ...
    

    le Minuteur only needs to be added in one place, but this adds a level of indentation to the whole definition of do_something().

A better solution is to use Minuteur comme un decorator. Decorators are powerful constructs that you use to modify the behavior of functions and classes. In this section, you’ll learn a little about how decorators work, how Minuteur can be extended to be a decorator, and how that will simplify timing functions. For a more in-depth explanation of decorators, see Primer on Python Decorators.

Understanding Decorators in Python

UNE decorator is a function that wraps another function to modify its behavior. This technique is possible because functions are first-class objects in Python. In other words, functions can be assigned to variables and used as arguments to other functions, just like any other object. This gives you a lot of flexibility and is the basis for several of Python’s more powerful features.

As a first example, you’ll create a decorator that does nothing:

def turn_off(func):
    revenir lambda *args, **kwargs: Aucun

First, note that turn_off() is just a regular function. What makes this a decorator is that it takes a function as its only argument and returns a function. You can use this to modify other functions like this:

>>>

>>> impression("Hello")
Bonjour

>>> impression = turn_off(impression)
>>> impression("Hush")
>>> # Nothing is printed

The line print = turn_off(print) decorates the print statement with the turn_off() decorator. Effectively, it replaces print() avec lambda *args, **kwargs: None returned by turn_off(). The lambda statement represents an anonymous function that does nothing except return Aucun.

For you to define more interesting decorators, you need to know about inner functions. Un inner function is a function defined inside another function. One common use of inner functions is to create function factories:

def create_multiplier(factor):
    def multiplier(num):
        revenir factor * num
    revenir multiplier

multiplier() is an inner function, defined inside create_multiplier(). Note that you have access to factor à l'intérieur multiplier(), tandis que multiplier() is not defined outside create_multiplier():

>>>

>>> multiplier
Traceback (most recent call last):
  Fichier "", line 1, in 
NameError: name 'multiplier' is not defined

Instead you use create_multiplier() to create new multiplier functions, each based on a different factor:

>>>

>>> double = create_multiplier(factor=2)
>>> double(3)
6

>>> quadruple = create_multiplier(factor=4)
>>> quadruple(sept)
28

Similarly, you can use inner functions to create decorators. Remember, a decorator is a function that returns a function:

    1 def triple(func):
    2     def wrapper_triple(*args, **kwargs):
    3         impression(f"Tripled func.__name__!r")
    4         valeur = func(*args, **kwargs)
    5         revenir valeur * 3
    6     revenir wrapper_triple

triple() is a decorator, because it’s a function that expects a function as it’s only argument, func(), and returns another function, wrapper_triple(). Note the structure of triple() itself:

  • Line 1 starts the definition of triple() and expects a function as an argument.
  • Lines 2 to 5 define the inner function wrapper_triple().
  • Line 6 Retour wrapper_triple().

This pattern is prevalent for defining decorators. The interesting parts are those happening inside the inner function:

  • Line 2 starts the definition of wrapper_triple(). This function will replace whichever function triple() decorates. The parameters are *args et **kwargs, which collect whichever positional and keyword arguments you pass to the function. This gives you the flexibility to use triple() on any function.
  • Line 3 prints out the name of the decorated function, and note that triple() has been applied to it.
  • Line 4 calls func(), the function that has been decorated by triple(). It passes on all arguments passed to wrapper_triple().
  • Line 5 triples the return value of func() and returns it.

Let’s try it out! knock() is a function that returns the word Penny. See what happens if it’s tripled:

>>>

>>> def knock():
...     revenir "Penny! "
...
>>> knock = triple(knock)
>>> result = knock()
Tripled 'knock'

>>> result
'Penny! Penny! Penny! "

Multiplying a text string by a number is a form of repetition, so Penny repeats three times. The decoration happens at knock = triple(knock).

It feels a bit clunky to keep repeating knock. Instead, PEP 318 introduced a more convenient syntax for applying decorators. The following definition of knock() does the same as the one above:

>>>

>>> @triple
... def knock():
...     revenir "Penny! "
...
>>> result = knock()
Tripled 'knock'

>>> result
'Penny! Penny! Penny! "

le @ symbol is used to apply decorators. In this case, @triple means that triple() is applied to the function defined just after it.

One of the few decorators defined in the standard library is @functools.wraps. This one is quite helpful when defining your own decorators. Since decorators effectively replace one function with another, they create a subtle issue with your functions:

>>>

>>> knock
<function triple..wrapper_triple at 0x7fa3bfe5dd90>

@triple decorates knock(), which is then replaced by the wrapper_triple() inner function, as the output above confirms. This will also replace the name, docstring, and other metadata. Often, this will not have much effect, but it can make introspection difficult.

Sometimes, decorated functions must have correct metadata. @functools.wraps fixes exactly this issue:

import functools

def triple(func):
    @functools.wraps(func)
    def wrapper_triple(*args, **kwargs):
        impression(f"Tripled func.__name__!r")
        valeur = func(*args, **kwargs)
        revenir valeur * 3
    revenir wrapper_triple

With this new definition of @triple, metadata are preserved:

>>>

>>> @triple
... def knock():
...     revenir "Penny! "
...
>>> knock

Notez que knock() now keeps its proper name, even after being decorated. It’s good form to use @functools.wraps whenever you define a decorator. A blueprint you can use for most of your decorators is the following:

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        valeur = func(*args, **kwargs)
        # Do something after
        revenir valeur
    revenir wrapper_decorator

To see more examples of how to define decorators, check out the examples listed in Primer on Python Decorators.

Creating a Python Timer Decorator

In this section, you’ll learn how to extend your Python timer so that you can use it as a decorator as well. However, as a first exercise, let’s create a Python timer decorator from scratch.

Based on the blueprint above, you only need to decide what to do before and after you call the decorated function. This is similar to the considerations about what to do when entering and exiting the context manager. You want to start a Python timer before calling the decorated function, and stop the Python timer after the call finishes. UNE @timer decorator can be defined as follows:

import functools
import temps

def timer(func):
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        tic = temps.perf_counter()
        valeur = func(*args, **kwargs)
        toc = temps.perf_counter()
        elapsed_time = toc - tic
        impression(f"Elapsed time: elapsed_time:0.4f    seconds")
        revenir valeur
    revenir wrapper_timer

Note how much wrapper_timer() resembles the early pattern you established for timing Python code. You can apply @timer comme suit:

>>>

>>> @timer
... def latest_tutorial():
...     Didacticiel = feed.get_article(0)
...     impression(Didacticiel)
...
>>> latest_tutorial()
# Python Timer Functions: Three Ways to Monitor Your Code

[ ... The full text of the tutorial ... ]
Elapsed time: 0.5414 seconds

Recall that you can also apply a decorator to a previously defined function:

>>>

>>> feed.get_article = timer(feed.get_article)

Puisque @ applies when functions are defined, you need to use the more basic form in these cases. One advantage of using a decorator is that you only need to apply it once, and it’ll time the function every time:

>>>

>>> Didacticiel = feed.get_article(0)
Elapsed time: 0.5512 seconds

@timer does the job. However, in a sense, you’re back to square one, since @timer does not have any of the flexibility or convenience of Minuteur. Can you also make your Minuteur class act like a decorator?

So far, you’ve used decorators as functions applied to other functions, but that’s not entirely correct. Decorators must be callables. There are many callable types in Python. You can make your own objects callable by defining the special .__call__() method in their class. The following function and class behave similarly:

>>>

>>> def carré(num):
...     revenir num ** 2
...
>>> carré(4)
16

>>> class Squarer:
...     def __call__(self, num):
...         revenir num ** 2
...
>>> carré = Squarer()
>>> carré(4)
16

Ici, carré is an instance that is callable and can square numbers, just like the square() function in the first example.

This gives you a way of adding decorator capabilities to the existing Minuteur class:

def __call__(self, func):
    """Support using Timer as a decorator"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        avec self:
            revenir func(*args, **kwargs)

    revenir wrapper_timer

.__call__() uses the fact that Minuteur is already a context manager to take advantage of the conveniences you’ve already defined there. Make sure you also import functools at the top of timer.py.

You can now use Minuteur as a decorator:

>>>

>>> @Timer(text="Downloaded the tutorial in :.2f    seconds")
... def latest_tutorial():
...     Didacticiel = feed.get_article(0)
...     impression(Didacticiel)
...
>>> latest_tutorial()
# Python Timer Functions: Three Ways to Monitor Your Code

[ ... The full text of the tutorial ... ]
Downloaded the tutorial in 0.72 seconds

Before rounding out this section, know that there’s a more straightforward way of turning your Python timer into a decorator. You’ve already seen some of the similarities between context managers and decorators. They’re both typically used to do something before and after executing some given code.

Based on these similarities, there’s a mixin class defined in the standard library called ContextDecorator. You can add decorator abilities to your context manager classes simply by inheriting ContextDecorator:

de contextlib import ContextDecorator

class Minuteur(ContextDecorator):
    # Implementation of Timer is unchanged

When you use ContextDecorator this way, there’s no need to implement .__call__() yourself, so you can safely delete it from the Minuteur class.

Using the Python Timer Decorator

Let’s redo the latest_tutorial.py example one last time, using the Python timer as a decorator:

    1 # latest_tutorial.py
    2 
    3 de timer import Minuteur
    4 de reader import feed
    5 
    6 @Timer()
    sept def principale():
    8     """Print the latest tutorial from Real Python"""
    9     Didacticiel = feed.get_article(0)
dix     impression(Didacticiel)
11 
12 si __name__ == "__main__":
13     principale()

If you compare this implementation with the original implementation without any timing, then you’ll notice that the only differences are the import of Minuteur on line 3 and the application of @Timer() on line 6. A significant advantage of using decorators is that they’re usually straightforward to apply, as you see here.

However, the decorator still applies to the whole function. This means your code is taking into account the time it takes to print the tutorial, in addition to the time it takes to download. Let’s run the script one final time:

$ python latest_tutorial.py
# Python Timer Functions: Three Ways to Monitor Your Code

[ ... The full text of the tutorial ... ]
Elapsed time: 0.69 seconds

The location of the elapsed time output is a tell-tale sign that your code is considering the time it takes to print time as well. As you see here, your code prints the elapsed time après the tutorial.

When you use Minuteur as a decorator, you’ll see similar advantages as you did with context managers:

  • Low effort: You only need one extra line of code to time the execution of a function.
  • Readability: When you add the decorator, you can note more clearly that your code will time the function.
  • Consistency: You only need to add the decorator when the function is defined. Your code will consistently time it every time it’s called.

However, decorators are not as flexible as context managers. You can only apply them to complete functions. It’s possible to add decorators to already defined functions, but this is a bit clunky and less common.

The Python Timer Code

You can expand the code block below to view the final source code for your Python timer:

# timer.py

de contextlib import ContextDecorator
de dataclasses import dataclass, field
import temps
de typing import Tout, Callable, ClassVar, Dict, Optional

class TimerError(Exception):
    """A custom exception used to report errors in use of Timer class"""

@dataclass
class Minuteur(ContextDecorator):
    """Time your code using a class, context manager, or decorator"""

    timers: ClassVar[[[[Dict[[[[str, float]] = dict()
    name: Optional[[[[str] = Aucun
    text: str = "Elapsed time: :0.4f    seconds"
    enregistreur: Optional[[[[Callable[[[[[[[[str], Aucun]] = impression
    _start_time: Optional[[[[float] = field(default=Aucun, init=Faux, repr=Faux)

    def __post_init__(self) -> Aucun:
        """Initialization: add timer to dict of timers"""
        si self.name:
            self.timers.setdefault(self.name, 0)

    def début(self) -> Aucun:
        """Start a new timer"""
        si self._start_time est ne pas Aucun:
            raise TimerError(f"Timer is running. Use .stop() to stop it")

        self._start_time = temps.perf_counter()

    def Arrêtez(self) -> float:
        """Stop the timer, and report the elapsed time"""
        si self._start_time est Aucun:
            raise TimerError(f"Timer is not running. Use .start() to start it")

        # Calculate elapsed time
        elapsed_time = temps.perf_counter() - self._start_time
        self._start_time = Aucun

        # Report elapsed time
        si self.enregistreur:
            self.enregistreur(self.text.format(elapsed_time))
        si self.name:
            self.timers[[[[self.name] += elapsed_time

        revenir elapsed_time

    def __enter__(self) -> "Timer":
        """Start a new timer as a context manager"""
        self.début()
        revenir self

    def __exit__(self, *exc_info: Tout) -> Aucun:
        """Stop the context manager timer"""
        self.Arrêtez()

The code is also available in the codetiming repository on GitHub.

You can use the code yourself by saving it to a file named timer.py and importing it into your program:

>>>

>>> de timer import Minuteur

Minuteur is also available on PyPI, so an even easier option is to install it using pip:

$ python -m pip install codetiming

Note that the package name on PyPI is codetiming. You’ll need to use this name both when you install the package and when you import Minuteur:

>>>

>>> de codetiming import Minuteur

Apart from this, codetiming.Timer works exactly as timer.Timer. To summarize, you can use Minuteur de trois manières différentes:

  1. Comme un class:

    t = Minuteur(name="class")
    t.début()
    # Do something
    t.Arrêtez()
    
  2. Comme un context manager:

    avec Minuteur(name="context manager"):
        # Do something
    
  3. Comme un decorator:

    @Timer(name="decorator")
    def des trucs():
        # Do something
    

This kind of Python timer is mainly useful for monitoring the time your code spends at individual key code blocks or functions. In the next section, you’ll get a quick overview of alternatives you can use if you’re looking to optimize your code.

Other Python Timer Functions

There are many options for timing your code with Python. In this tutorial, you’ve learned how to create a flexible and convenient class that you can use in several different ways. A quick search on PyPI shows that there are already many projects available that offer Python timer solutions.

In this section, you’ll first learn more about the different functions available in the standard library for measuring time, and why perf_counter() is preferable. Then, you’ll see alternatives for optimizing your code, for which Minuteur is not well-suited.

Using Alternative Python Timer Functions

You’ve been using perf_counter() throughout this tutorial to do the actual time measurements, but Python’s temps library comes with several other functions that also measure time. Here are some alternatives:

One of the reasons why there are several functions is that Python represents time as a float. Floating-point numbers are inaccurate by nature. You may have seen results like these before:

>>>

>>> 0,1 + 0,1 + 0,1
0.30000000000000004

>>> 0,1 + 0,1 + 0,1 == 0.3
Faux

Python’s float follows the IEEE 754 Standard for Floating-Point Arithmetic, which tries to represent all floating-point numbers in 64 bits. Since there are infinitely many floating-point numbers, you can’t express them all with a finite number of bits.

IEEE 754 prescribes a system where the density of numbers that you can represent varies. The closer you are to 1, the more numbers you can represent. For larger numbers, there’s more espace between the numbers that you can express. This has some consequences when you use a float to represent time.

Considérer time(). The main purpose of this function is to represent the actual time right now. It does this as the number of seconds since a given point in time, called the epoch. The number returned by time() is quite big, which means that there are fewer numbers available, and the resolution suffers. Specifically, time() is not able to measure nanosecond differences:

>>>

>>> import temps
>>> t = temps.temps()
>>> t
1564342757.0654016

>>> t + 1e-9
1564342757.0654016

>>> t == t + 1e-9
True

A nanosecond is one-billionth of a second. Note that adding a nanosecond to t does not affect the result. perf_counter(), on the other hand, uses some undefined point in time as its epoch, allowing it to work with smaller numbers and therefore obtain a better resolution:

>>>

>>> import temps
>>> p = temps.perf_counter()
>>> p
11370.015653846

>>> p + 1e-9
11370.015653847

>>> p == p + 1e-9
Faux

Here, you see that adding a nanosecond to p actually affects the outcome. For more information about how to work with time(), see A Beginner’s Guide to the Python time Module.

The challenges with representing time as a float are well known, so Python 3.7 introduced a new option. Chaque temps measurement function now has a corresponding _ns function that returns the number of nanoseconds as an int instead of the number of seconds as a float. For instance, time() now has a nanosecond counterpart called time_ns():

>>>

>>> import temps
>>> temps.time_ns()
1564342792866601283

Integers are unbounded in Python, so this allows time_ns() to give nanosecond resolution for all eternity. De même, perf_counter_ns() is a nanosecond variant of perf_counter():

>>>

>>> import temps
>>> temps.perf_counter()
13580.153084446

>>> temps.perf_counter_ns()
13580765666638

Puisque perf_counter() already provides nanosecond resolution, there are fewer advantages to using perf_counter_ns().

There are two functions in temps that do not measure the time spent sleeping. Ceux-ci sont process_time() et thread_time(), which are useful in some settings. However, for Minuteur, you typically want to measure the full time spent. The final function in the list above is monotonic(). The name alludes to this function being a monotonic timer, which is a Python timer that can never move backward.

All these functions are monotonic except time(), which can go backward if the system time is adjusted. On some systems, monotonic() is the same function as perf_counter(), and you can use them interchangeably. However, this is not always the case. You can use time.get_clock_info() to get more information about a Python timer function. Using Python 3.7 on Linux I get the following information:

>>>

>>> import temps
>>> temps.get_clock_info("monotonic")
namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)',
                                        monotonic=True, resolution=1e-09)

>>> temps.get_clock_info("perf_counter")
namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)',
                                        monotonic=True, resolution=1e-09)

The results could be different on your system.

PEP 418 describes some of the rationale behind introducing these functions. It includes the following short descriptions:

  • time.monotonic(): timeout and scheduling, not affected by system clock updates
  • time.perf_counter(): benchmarking, most precise clock for short period
  • time.process_time(): profiling, CPU time of the process (Source)

As you can see, it’s usually the best choice for you to use perf_counter() for your Python timer.

Estimating Running Time With timeit

Say you’re trying to squeeze the last bit of performance out of your code, and you’re wondering about the most effective way to convert a list to a set. You want to compare using set() and the set literal, .... You can use your Python timer for this:

>>>

>>> de timer import Minuteur
>>> Nombres = [[[[sept, 6, 1, 4, 1, 8, 0, 6]
>>> avec Minuteur(text=":.8f"):
...     ensemble(Nombres)
...
0, 1, 4, 6, 7, 8
0.00007373

>>> avec Minuteur(text=":.8f"):
...     *Nombres
...
0, 1, 4, 6, 7, 8
0.00006204

This test seems to indicate that the set literal might be slightly faster. However, these results are quite uncertain, and if you rerun the code, you might get wildly different results. That’s because you’re only trying the code once. You could, for instance, get unlucky and run the script just as your computer is becoming busy with other tasks.

A better way is to use the timeit standard library. It’s designed precisely to measure the execution time of small code snippets. While you can import and call timeit.timeit() from Python as a regular function, it is usually more convenient to use the command-line interface. You can time the two variants as follows:

$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)"
2000000 loops, best of 5: 163 nsec per loop

$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "*nums"
2000000 loops, best of 5: 121 nsec per loop

timeit automatically calls your code many times to average out noisy measurements. The results from timeit confirm that the set literal is faster than set(). You can find more information about this particular issue at Michael Bassili’s blog.

Finally, the IPython interactive shell and the Jupyter notebook have extra support for this functionality with the %timeit magic command:

>>>

Dans [1]: Nombres = [[[[sept, 6, 1, 4, 1, 8, 0, 6]

Dans [2]: %timeit set(numbers)
171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Dans [3]: %timeit *numbers
147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Again, the measurements indicate that using a set literal is faster. In Jupyter Notebooks you can also use the %%timeit cell-magic to measure the time of running a whole cell.

Finding Bottlenecks in Your Code With Profilers

timeit is excellent for benchmarking a particular snippet of code. However, it would be very cumbersome to use it to check all parts of your program and locate which sections take the most time. Instead, you can use a profiler.

cProfile is a profiler that you can access at any time from the standard library. You can use it in several ways, although it’s usually most straightforward to use it as a command-line tool:

$ python -m cProfile -o latest_tutorial.prof latest_tutorial.py

This command runs latest_tutorial.py with profiling turned on. You save the output from cProfile dans latest_tutorial.prof, as specified by the -o option. The output data is in a binary format that needs a dedicated program to make sense of it. Again, Python has an option right in the standard library! Runnin the pstats module on your .prof file opens an interactive profile statistics browser:

$ python -m pstats latest_tutorial.prof
Welcome to the profile statistics browser.
latest_tutorial.prof% help

Documented commands (type help ):
========================================
EOF  add  callees  callers  help  quit  read  reverse  sort  stats  strip

To use pstats you type commands at the prompt. Here you can see the integrated help system. Typically you’ll use the sort et stats commands. To get a cleaner output, strip can be useful:

latest_tutorial.prof% strip
latest_tutorial.prof% sort cumtime
latest_tutorial.prof% stats 10
                                    1393801 function calls (1389027 primitive calls) in 0.586 seconds

    Ordered by: cumulative time
    List reduced from 1443 to 10 due to restriction <10>

    ncalls tottime percall cumtime percall filename:lineno(function)
        144/1   0.001   0.000   0.586   0.586 built-in method builtins.exec
                        1   0.000   0.000   0.586   0.586 latest_tutorial.py:3()
                        1   0.000   0.000   0.521   0.521 contextlib.py:71(inner)
                        1   0.000   0.000   0.521   0.521 latest_tutorial.py:6(read_latest_tutorial)
                        1   0.000   0.000   0.521   0.521 feed.py:28(get_article)
                        1   0.000   0.000   0.469   0.469 feed.py:15(_feed)
                        1   0.000   0.000   0.469   0.469 feedparser.py:3817(parse)
                        1   0.000   0.000   0.271   0.271 expatreader.py:103(parse)
                        1   0.000   0.000   0.271   0.271 xmlreader.py:115(parse)
                    13   0.000   0.000   0.270   0.021 expatreader.py:206(feed)

This output shows that the total runtime was 0.586 seconds. It also lists the ten functions where your code spent most of its time. Here you’ve sorted by cumulative time (cumtime), which means that your code counts time when the given function has called another function.

You can see that your code spends virtually all its time inside the latest_tutorial module, and in particular, inside read_latest_tutorial(). While this might be useful confirmation of what you already know, it’s often more interesting to find where your code actually spends time.

The total time (tottime) column indicates how much time your code spent inside a function, excluding time in sub-functions. You can see that none of the functions above really spend any time doing this. To find where the code spent most of its time, issue another sort commander:

latest_tutorial.prof% sort tottime
latest_tutorial.prof% stats 10
                                    1393801 function calls (1389027 primitive calls) in 0.586 seconds

    Ordered by: internal time
    List reduced from 1443 to 10 due to restriction <10>

    ncalls tottime percall cumtime percall filename:lineno(function)
                    59   0.091   0.002   0.091   0.002 method 'read' of '_ssl._SSLSocket'
    114215   0.070   0.000   0.099   0.000 feedparser.py:308(__getitem__)
    113341   0.046   0.000   0.173   0.000 feedparser.py:756(handle_data)
                        1   0.033   0.033   0.033   0.033 method 'do_handshake' of '_ssl._SSLSocket'
                        1   0.029   0.029   0.029   0.029 method 'connect' of '_socket.socket'
                    13   0.026   0.002   0.270   0.021 method 'Parse' of 'pyexpat.xmlparser'
    113806   0.024   0.000   0.123   0.000 feedparser.py:373(get)
            3455   0.023   0.000   0.024   0.000 method 'sub' of 're.Pattern'
    113341   0.019   0.000   0.193   0.000 feedparser.py:2033(characters)
                236   0.017   0.000   0.017   0.000 method 'translate' of 'str'

You can now see that latest_tutorial.py actually spends most of its time working with sockets or handling data inside feedparser. The latter is one of the dependencies of the Real Python Reader that’s used to parse the tutorial feed.

You can use pstats to get some idea on where your code is spending most of its time and see if you can optimize any bottlenecks you find. You can also use the tool to understand the structure of your code better. For instance, the commands callees et callers will show you which functions call and are called by a given function.

You can also investigate certain functions. Let’s see how much overhead Minuteur causes by filtering the results with the phrase timer:

latest_tutorial.prof% stats timer
                                    1393801 function calls (1389027 primitive calls) in 0.586 seconds

    Ordered by: internal time
    List reduced from 1443 to 8 due to restriction <'timer'>

    ncalls tottime percall cumtime percall filename:lineno(function)
                        1   0.000   0.000   0.000   0.000 timer.py:13(Timer)
                        1   0.000   0.000   0.000   0.000 timer.py:35(stop)
                        1   0.000   0.000   0.003   0.003 timer.py:3()
                        1   0.000   0.000   0.000   0.000 timer.py:28(start)
                        1   0.000   0.000   0.000   0.000 timer.py:9(TimerError)
                        1   0.000   0.000   0.000   0.000 timer.py:23(__post_init__)
                        1   0.000   0.000   0.000   0.000 timer.py:57(__exit__)
                        1   0.000   0.000   0.000   0.000 timer.py:52(__enter__)

Heureusement, Minuteur causes only minimal overhead. Utilisation quit to leave the pstats browser when you’re done investigating.

For a more powerful interface into profile data, check out KCacheGrind. It uses its own data format, but you can convert data from cProfile en utilisant pyprof2calltree:

$ pyprof2calltree -k -i latest_tutorial.prof

This command will convert latest_tutorial.prof and open KCacheGrind to analyze the data.

The last option you’ll see here for timing your code is line_profiler. cProfile can tell you which functions your code spends the most time in, but it won’t give you insights into which lines inside that function are the slowest. That’s where line_profiler can help you.

Note that line profiling takes time and adds a fair bit of overhead to your runtime. A more standard workflow is first to use cProfile to identify which functions to look at and then run line_profiler on those functions. line_profiler is not part of the standard library, so you should first follow the installation instructions to set it up.

Before you run the profiler, you need to tell it which functions to profile. You do this by adding a @profile decorator inside your source code. For example, to profile Timer.stop() you add the following inside timer.py:

@profile
def Arrêtez(self) -> float:
    # The rest of the code is unchanged

Note that you don’t import profil anywhere. Instead, it’s automatically added to the global namespace when you run the profiler. You need to delete the line when you’re done profiling, though. Otherwise, you’ll get a NameError.

Next, run the profiler using kernprof, which is part of the line_profiler package:

$ kernprof -l latest_tutorial.py

This command automatically saves the profiler data in a file called latest_tutorial.py.lprof. You can see those results using line_profiler:

$ python -m line_profiler latest_tutorial.py.lprof
Timer unit: 1e-06 s

Total time: 1.6e-05 s
File: /home/realpython/timer.py
Function: stop at line 35

# Hits Time PrHit %Time Line Contents
=====================================
35                      @profile
36                      def stop(self) -> float:
37                          """Stop the timer, and report the elapsed time"""
38  1   1.0   1.0   6.2     if self._start_time is None:
39                              raise TimerError(f"Timer is not running. ...")
40
41                          # Calculate elapsed time
42  1   2.0   2.0  12.5     elapsed_time = time.perf_counter() - self._start_time
43  1   0.0   0.0   0.0     self._start_time = None
44
45                          # Report elapsed time
46  1   0.0   0.0   0.0     if self.logger:
47  1  11.0  11.0  68.8         self.logger(self.text.format(elapsed_time))
48  1   1.0   1.0   6.2     if self.name:
49  1   1.0   1.0   6.2         self.timers[self.name] += elapsed_time
50
51  1   0.0   0.0   0.0     return elapsed_time

First, note that the time unit in this report is microseconds (1e-06 s). Usually, the most accessible number to look at is %Time, which tells you the percentage of the total time your code spends inside a function at each line. In this example, you can see that your code spends almost 70% of the time on line 47, which is the line that formats and prints the result of the timer.

Conclusion

In this tutorial, you’ve seen several different approaches to adding a Python timer to your code:

  • You used a class to keep state and add a user-friendly interface. Classes are very flexible, and using Minuteur directly gives you full control over how and when to invoke the timer.

  • You used a context manager to add features to a block of code and, if necessary, to clean up afterward. Context managers are straightforward to use, and adding with Timer() can help you more clearly distinguish your code visually.

  • You used a decorator to add behavior to a function. Decorators are concise and compelling, and using @Timer() is a quick way to monitor your code’s runtime.

You’ve also seen why you should prefer time.perf_counter() plus de time.time() when benchmarking code, as well as what other alternatives are useful when you’re optimizing your code.

Now you can add Python timer functions to your own code! Keeping track of how fast your program runs in your logs will help you monitor your scripts. Do you have ideas for other use cases where classes, context managers, and decorators play well together? Leave a comment down below!

Ressources

For a deeper dive into Python timer functions, check out these resources:

  • codetiming is the Python timer available on PyPI.
  • time.perf_counter() is a performance counter for precise timings.
  • timeit is a tool for comparing the runtimes of code snippets.
  • cProfile is a profiler for finding bottlenecks in scripts and programs.
  • pstats is a command-line tool for looking at profiler data.
  • KCachegrind is a GUI for looking at profiler data.
  • line_profiler is a profiler for measuring individual lines of code.
  • memory-profiler is a profiler for monitoring memory usage.

[ad_2]