Votre guide de la fonction d’impression Python

Votre guide de la fonction d'impression Python

Si vous êtes comme la plupart des utilisateurs de Python, moi y compris, vous avez probablement commencé votre aventure Python en découvrantprint(). Cela vous a aidé à écrire votre propre one-linerhello world. Vous pouvez l'utiliser pour afficher des messages formatés à l'écran et peut-être trouver des bogues. Mais si vous pensez que c'est tout ce qu'il y a à savoir sur la fonctionprint()de Python, alors vous manquez beaucoup de choses!

Continuez à lire pour profiter pleinement de cette petite fonction apparemment ennuyeuse et non appréciée. Ce tutoriel vous permettra de vous familiariser avec l'utilisation efficace de Pythonprint(). Cependant, préparez-vous à une plongée profonde en parcourant les sections. Vous serez peut-être surpris de tout ce queprint() a à offrir!

À la fin de ce didacticiel, vous saurez comment:

  • Évitez les erreurs courantes avec lesprint() de Python

  • Traitez les nouvelles lignes, les encodages de caractères et la mise en mémoire tampon

  • Écrire du texte dans des fichiers

  • Mockprint() dans les tests unitaires

  • Créer des interfaces utilisateur avancées dans le terminal

Si vous êtes un débutant complet, vous bénéficierez le plus de la lecture de la première partie de ce didacticiel, qui illustre les éléments essentiels de l'impression en Python. Sinon, n'hésitez pas à sauter cette partie et à sauter comme bon vous semble.

Note:print() était un ajout majeur à Python 3, dans lequel il remplaçait l'ancienne instructionprint disponible dans Python 2.

Il y avait plusieurs bonnes raisons à cela, comme vous le verrez bientôt. Bien que ce didacticiel se concentre sur Python 3, il montre l'ancienne méthode d'impression en Python pour référence.

Free Bonus:Click here to get our free Python Cheat Sheet qui vous montre les bases de Python 3, comme travailler avec des types de données, des dictionnaires, des listes et des fonctions Python.

L'impression en bref

Commençons par regarder quelques exemples réels d'impression en Python. À la fin de cette section, vous connaîtrez tous les moyens d'appelerprint(). Ou, dans le jargon des programmeurs, vous diriez que vous serez familier avec lesfunction signature.

Appel d'impression

L'exemple le plus simple d'utilisation de Pythonprint() ne nécessite que quelques touches:

>>>

>>> print()

Vous ne passez aucun argument, mais vous devez tout de même mettre des parenthèses vides à la fin, ce qui indique à Python de faire en faitexecute the function plutôt que de simplement s'y référer par son nom.

Cela produira un caractère de nouvelle ligne invisible, qui à son tour fera apparaître une ligne vierge sur votre écran. Vous pouvez appelerprint() plusieurs fois comme ceci pour ajouter un espace vertical. C'est comme si vous tapiez[.kbd .key-enter]#Enter # sur votre clavier dans un traitement de texte.

Comme vous venez de le voir, appelerprint() sans arguments aboutit à unblank line, qui est une ligne composée uniquement du caractère de nouvelle ligne. Ne confondez pas cela avec unempty line, qui ne contient aucun caractère, pas même la nouvelle ligne!

Vous pouvez utiliser les littérauxstring de Python pour visualiser ces deux:

'\n'  # Blank line
''    # Empty line

Le premier est long d'un caractère, tandis que le second n'a pas de contenu.

Note: Pour supprimer le caractère de nouvelle ligne d'une chaîne en Python, utilisez sa méthode.rstrip(), comme ceci:

>>>

>>> 'A line of text.\n'.rstrip()
'A line of text.'

Cela supprime tout espace de fin du bord droit de la chaîne de caractères.

Dans un scénario plus courant, vous souhaitez communiquer un message à l'utilisateur final. Il existe plusieurs moyens d'y parvenir.

Tout d'abord, vous pouvez passer un littéral de chaîne directement àprint():

>>>

>>> print('Please wait while the program is loading...')

Cela imprimera le message textuellement sur l'écran.

Deuxièmement, vous pouvez extraire ce message dans sa propre variable avec un nom significatif pour améliorer la lisibilité et promouvoir la réutilisation du code:

>>>

>>> message = 'Please wait while the program is loading...'
>>> print(message)

Enfin, vous pouvez passer une expression, commestring concatenation, à évaluer avant d'imprimer le résultat:

>>>

>>> import os
>>> print('Hello, ' + os.getlogin() + '! How are you?')
Hello, jdoe! How are you?

En fait, il existe une douzaine de façons de formater les messages en Python. Je vous encourage vivement à jeter un œil àf-strings, introduit dans Python 3.6, car ils offrent la syntaxe la plus concise de toutes:

>>>

>>> import os
>>> print(f'Hello, {os.getlogin()}! How are you?')

De plus, les chaînes f vous empêcheront de commettre une erreur courante, qui est d'oublier de taper des opérandes concaténés cast. Python est un langage fortement typé, ce qui signifie qu'il ne vous permettra pas de faire ceci:

>>>

>>> 'My age is ' + 42
Traceback (most recent call last):
  File "", line 1, in 
    'My age is ' + 42
TypeError: can only concatenate str (not "int") to str

C'est faux parce que l'ajout de nombres aux chaînes n'a pas de sens. Vous devez d'abord convertir explicitement le nombre en chaîne, afin de les joindre ensemble:

>>>

>>> 'My age is ' + str(42)
'My age is 42'

Sauf si vous voushandle such errorsvous-même, l'interpréteur Python vous informera d'un problème en affichant untraceback.

Note:str() est une fonction intégrée globale qui convertit un objet en sa représentation sous forme de chaîne.

Vous pouvez l'appeler directement sur n'importe quel objet, par exemple un numéro:

>>>

>>> str(3.14)
'3.14'

Les types de données intégrés ont une représentation de chaîne prédéfinie, mais plus loin dans cet article, vous découvrirez comment en fournir une pour vos classes personnalisées.

Comme pour toute fonction, peu importe que vous passiez un littéral, une variable ou une expression. Contrairement à de nombreuses autres fonctions, cependant,print() acceptera tout quel que soit son type.

Jusqu'à présent, vous n'avez regardé que la chaîne, mais qu'en est-il des autres types de données? Essayons des littéraux de différents types intégrés et voyons ce qui en sort:

>>>

>>> print(42)                            # 
42
>>> print(3.14)                          # 
3.14
>>> print(1 + 2j)                        # 
(1+2j)
>>> print(True)                          # 
True
>>> print([1, 2, 3])                     # 
[1, 2, 3]
>>> print((1, 2, 3))                     # 
(1, 2, 3)
>>> print({'red', 'green', 'blue'})      # 
{'red', 'green', 'blue'}
>>> print({'name': 'Alice', 'age': 42})  # 
{'name': 'Alice', 'age': 42}
>>> print('hello')                       # 
hello

Attention cependant à la constanteNone. Bien qu'il soit utilisé pour indiquer une absence de valeur, il apparaîtra comme'None' plutôt que comme une chaîne vide:

>>>

>>> print(None)
None

Commentprint() sait-il travailler avec tous ces différents types? Eh bien, la réponse courte est que non. Il appelle implicitementstr() dans les coulisses pour taper cast n'importe quel objet en une chaîne. Ensuite, il traite les cordes de manière uniforme.

Plus loin dans ce didacticiel, vous apprendrez à utiliser ce mécanisme pour imprimer des types de données personnalisés tels que vos classes.

D'accord, vous pouvez maintenant appelerprint() avec un seul argument ou sans aucun argument. Vous savez imprimer des messages fixes ou formatés sur l'écran. La prochaine sous-section développera un peu la mise en forme des messages.

Séparation de plusieurs arguments

Vous avez vuprint() appelé sans aucun argument pour produire une ligne vide, puis appelé avec un seul argument pour afficher un message fixe ou formaté.

Cependant, il s'avère que cette fonction peut accepter n'importe quel nombre depositional arguments, y compris zéro, un ou plusieurs arguments. C'est très pratique dans un cas courant de mise en forme des messages, où vous souhaitez associer quelques éléments.

Jetons un œil à cet exemple:

>>>

>>> import os
>>> print('My name is', os.getlogin(), 'and I am', 42)
My name is jdoe and I am 42

print() a concaténé les quatre arguments qui lui sont passés, et il a inséré un seul espace entre eux afin que vous ne vous retrouviez pas avec un message écrasé comme'My name isjdoeand I am42'.

Notez qu'il a également pris soin de la conversion de type appropriée en appelant implicitementstr() sur chaque argument avant de les joindre. Si vous vous souvenez de la sous-section précédente, une concaténation naïve peut facilement entraîner une erreur en raison de types incompatibles:

>>>

>>> print('My age is: ' + 42)
Traceback (most recent call last):
  File "", line 1, in 
    print('My age is: ' + 42)
TypeError: can only concatenate str (not "int") to str

En plus d'accepter un nombre variable d'arguments positionnels,print() définit quatre nommés oukeyword arguments, qui sont facultatifs car ils ont tous des valeurs par défaut. Vous pouvez consulter leur brève documentation en appelanthelp(print) depuis l'interpréteur interactif.

Concentrons-nous sursep juste pour le moment. Il représenteseparator et se voit attribuer un seul espace (' ') par défaut. Il détermine la valeur avec laquelle joindre les éléments.

Il doit s'agir d'une chaîne ou deNone, mais ce dernier a le même effet que l'espace par défaut:

>>>

>>> print('hello', 'world', sep=None)
hello world
>>> print('hello', 'world', sep=' ')
hello world
>>> print('hello', 'world')
hello world

Si vous souhaitez supprimer complètement le séparateur, vous devez passer une chaîne vide ('') à la place:

>>>

>>> print('hello', 'world', sep='')
helloworld

Vous souhaiterez peut-être queprint() joint ses arguments sous forme de lignes séparées. Dans ce cas, passez simplement le caractère de nouvelle ligne échappé décrit précédemment:

>>>

>>> print('hello', 'world', sep='\n')
hello
world

Un exemple plus utile du paramètresep serait d'imprimer quelque chose comme les chemins de fichiers:

>>>

>>> print('home', 'user', 'documents', sep='/')
home/user/documents

N'oubliez pas que le séparateur vient entre les éléments, pas autour d'eux, vous devez donc en tenir compte d'une manière ou d'une autre:

>>>

>>> print('/home', 'user', 'documents', sep='/')
/home/user/documents
>>> print('', 'home', 'user', 'documents', sep='/')
/home/user/documents

Plus précisément, vous pouvez insérer une barre oblique (/) dans le premier argument positionnel, ou utiliser une chaîne vide comme premier argument pour appliquer la barre oblique.

Note: Soyez prudent lorsque vous joignez des éléments d'une liste ou d'un tuple.

Si vous le faites manuellement, vous obtiendrez unTypeError bien connu si au moins un des éléments n’est pas une chaîne:

>>>

>>> print(' '.join(['jdoe is', 42, 'years old']))
Traceback (most recent call last):
  File "", line 1, in 
    print(','.join(['jdoe is', 42, 'years old']))
TypeError: sequence item 1: expected str instance, int found

Il est plus sûr de simplement décompresser la séquence avec l’opérateur étoile (*) et de laisserprint() gérer le type de conversion:

>>>

>>> print(*['jdoe is', 42, 'years old'])
jdoe is 42 years old

Le déballage est en fait identique à l'appel deprint() avec des éléments individuels de la liste.

Un autre exemple intéressant pourrait être l'exportation de données au formatcomma-separated values (CSV):

>>>

>>> print(1, 'Python Tricks', 'Dan Bader', sep=',')
1,Python Tricks,Dan Bader

Cela ne traiterait pas correctement les cas marginaux tels que les virgules d'échappement, mais pour les cas d'utilisation simples, il devrait le faire. La ligne ci-dessus apparaîtra dans votre fenêtre de terminal. Pour l'enregistrer dans un fichier, vous devez rediriger la sortie. Plus loin dans cette section, vous verrez comment utiliserprint() pour écrire du texte dans des fichiers directement à partir de Python.

Enfin, le paramètresep n'est pas limité à un seul caractère. Vous pouvez joindre des éléments avec des chaînes de n'importe quelle longueur:

>>>

>>> print('node', 'child', 'child', sep=' -> ')
node -> child -> child

Dans les sous-sections à venir, vous explorerez les arguments de mots clés restants de la fonctionprint().

Prévention des sauts de ligne

Parfois, vous ne souhaitez pas terminer votre message par une nouvelle ligne de fin de sorte que les appels suivants àprint() se poursuivent sur la même ligne. Les exemples classiques incluent la mise à jour de la progression d'une opération de longue durée ou l'invitation de l'utilisateur à entrer. Dans ce dernier cas, vous souhaitez que l'utilisateur tape la réponse sur la même ligne:

Are you sure you want to do this? [y/n] y

De nombreux langages de programmation exposent des fonctions similaires àprint() via leurs bibliothèques standard, mais ils vous permettent de décider d'ajouter ou non une nouvelle ligne. Par exemple, en Java et C #, vous avez deux fonctions distinctes, tandis que d'autres langages vous obligent à ajouter explicitement à la fin d'une chaîne littérale.

Voici quelques exemples de syntaxe dans de tels langages:

Langue Exemple

Perl

print "hello world\n"

C

printf("hello world\n");

C++

std::cout << "hello world" << std::endl;

En revanche, la fonctionprint() de Python ajoute toujours sans demander, car c'est ce que vous voulez dans la plupart des cas. Pour le désactiver, vous pouvez profiter d'un autre argument de mot-clé,end, qui dicte par quoi terminer la ligne.

En termes de sémantique, le paramètreend est presque identique à celui desep que vous avez vu précédemment:

  • Ce doit être une chaîne ouNone.

  • Cela peut être arbitrairement long.

  • Il a une valeur par défaut de' '.

  • S'il est égal àNone, il aura le même effet que la valeur par défaut.

  • S'il est égal à une chaîne vide (''), il supprimera la nouvelle ligne.

Vous comprenez maintenant ce qui se passe sous le capot lorsque vous appelezprint() sans arguments. Comme vous ne fournissez aucun argument positionnel à la fonction, il n'y a rien à joindre, et donc le séparateur par défaut n'est pas du tout utilisé. Cependant, la valeur par défaut deend s'applique toujours et une ligne vide apparaît.

Note: Vous vous demandez peut-être pourquoi le paramètreend a une valeur par défaut fixe plutôt que ce qui a du sens sur votre système d'exploitation.

Eh bien, vous n’avez pas à vous soucier de la représentation de nouvelle ligne sur différents systèmes d’exploitation lors de l’impression, carprint() gérera automatiquement la conversion. N'oubliez pas de toujours utiliser la séquence d'échappement de dans les chaînes littérales.

C'est actuellement la façon la plus portable d'imprimer un caractère de nouvelle ligne en Python:

>>>

>>> print('line1\nline2\nline3')
line1
line2
line3

Si vous deviez essayer d'imprimer de force un caractère de nouvelle ligne spécifique à Windows sur une machine Linux, par exemple, vous vous retrouveriez avec une sortie cassée:

>>>

>>> print('line1\r\nline2\r\nline3')


line3

D'un autre côté, lorsque vous ouvrez un fichier en lecture avecopen(), vous n'avez pas non plus besoin de vous soucier de la représentation de nouvelle ligne. La fonction traduira toute nouvelle ligne spécifique au système rencontrée en un' ' universel. Dans le même temps, vous avez le contrôle sur la façon dont les sauts de ligne doivent être traités à la fois en entrée et en sortie si vous en avez vraiment besoin.

Pour désactiver la nouvelle ligne, vous devez spécifier une chaîne vide via l'argument de mot-cléend:

print('Checking file integrity...', end='')
# (...)
print('ok')

Même s'il s'agit de deux appelsprint() séparés, qui peuvent s'exécuter longtemps à part, vous ne verrez finalement qu'une seule ligne. Tout d'abord, cela ressemblera à ceci:

Checking file integrity...

Cependant, après le deuxième appel àprint(), la même ligne apparaîtra à l'écran comme:

Checking file integrity...ok

Comme avecsep, vous pouvez utiliserend pour joindre des éléments individuels en une grosse goutte de texte avec un séparateur personnalisé. Au lieu de joindre plusieurs arguments, cependant, il ajoutera le texte de chaque appel de fonction à la même ligne:

print('The first sentence', end='. ')
print('The second sentence', end='. ')
print('The last sentence.')

Ces trois instructions afficheront une seule ligne de texte:

The first sentence. The second sentence. The last sentence.

Vous pouvez mélanger les deux arguments de mots clés:

print('Mercury', 'Venus', 'Earth', sep=', ', end=', ')
print('Mars', 'Jupiter', 'Saturn', sep=', ', end=', ')
print('Uranus', 'Neptune', 'Pluto', sep=', ')

Non seulement vous obtenez une seule ligne de texte, mais tous les éléments sont séparés par une virgule:

Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto

Rien ne vous empêche d'utiliser le caractère de nouvelle ligne avec un rembourrage supplémentaire autour:

print('Printing in a Nutshell', end='\n * ')
print('Calling Print', end='\n * ')
print('Separating Multiple Arguments', end='\n * ')
print('Preventing Line Breaks')

Il imprimerait le texte suivant:

Printing in a Nutshell
 * Calling Print
 * Separating Multiple Arguments
 * Preventing Line Breaks

Comme vous pouvez le voir, l'argument de mot-cléend acceptera des chaînes arbitraires.

Note: Une boucle sur des lignes dans un fichier texte préserve leurs propres caractères de nouvelle ligne, ce qui, combiné avec le comportement par défaut de la fonctionprint(), entraînera un caractère de nouvelle ligne redondant:

>>>

>>> with open('file.txt') as file_object:
...     for line in file_object:
...         print(line)
...
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod

tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,

quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo

Il y a deux nouvelles lignes après chaque ligne de texte. Vous souhaitez supprimer l'un d'eux, comme indiqué plus haut dans cet article, avant d'imprimer la ligne:

print(line.rstrip())

Vous pouvez également conserver la nouvelle ligne dans le contenu mais supprimer automatiquement celle ajoutée parprint(). Vous utiliserez l'argument de mot-cléend pour ce faire:

>>>

>>> with open('file.txt') as file_object:
...     for line in file_object:
...         print(line, end='')
...
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo

En terminant une ligne avec une chaîne vide, vous désactivez effectivement l'une des nouvelles lignes.

Vous vous familiarisez davantage avec l'impression en Python, mais il reste encore beaucoup d'informations utiles à venir. Dans la sous-section à venir, vous apprendrez comment intercepter et rediriger la sortie de la fonctionprint().

Impression dans un fichier

Croyez-le ou non,print() ne sait pas comment transformer des messages en texte sur votre écran, et franchement, ce n’est pas nécessaire. C'est un travail pour les couches de code de niveau inférieur, qui comprennent les octets et savent comment les déplacer.

print() est une abstraction sur ces couches, fournissant une interface pratique qui délègue simplement l'impression réelle à un flux ou àfile-like object. Un flux peut être n'importe quel fichier sur votre disque, une prise réseau ou peut-être un tampon en mémoire.

En plus de cela, il existe trois flux standard fournis par le système d'exploitation:

  1. Entrée standardstdin:

  2. Sortie standardstdout:

  3. Erreur standardstderr:

En Python, vous pouvez accéder à tous les flux standard via le module intégrésys:

>>>

>>> import sys
>>> sys.stdin
<_io.TextIOWrapper name='' mode='r' encoding='UTF-8'>
>>> sys.stdin.fileno()
0
>>> sys.stdout
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> sys.stdout.fileno()
1
>>> sys.stderr
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> sys.stderr.fileno()
2

Comme vous pouvez le voir, ces valeurs prédéfinies ressemblent à des objets de type fichier avec les attributsmode etencoding ainsi qu'aux méthodes.read() et.write() parmi beaucoup d'autres.

Par défaut,print() est lié àsys.stdout via son argumentfile, mais vous pouvez changer cela. Utilisez cet argument de mot-clé pour indiquer un fichier qui était ouvert en mode écriture ou ajout, afin que les messages y accèdent directement:

with open('file.txt', mode='w') as file_object:
    print('hello world', file=file_object)

Cela rendra votre code à l'abri de la redirection de flux au niveau du système d'exploitation, ce qui pourrait ou non être souhaité.

Pour plus d'informations surworking with files in Python, vous pouvez consulterReading and Writing Files in Python (Guide).

Note: N'essayez pas d'utiliserprint() pour écrire des données binaires car il ne convient que pour le texte.

Appelez simplement les.write() du fichier binaire directement:

with open('file.dat', 'wb') as file_object:
    file_object.write(bytes(4))
    file_object.write(b'\xff')

Si vous vouliez écrire des octets bruts sur la sortie standard, cela échouera également carsys.stdout est un flux de caractères:

>>>

>>> import sys
>>> sys.stdout.write(bytes(4))
Traceback (most recent call last):
  File "", line 1, in 
TypeError: write() argument must be str, not bytes

Vous devez creuser plus profondément pour obtenir une poignée du flux d'octets sous-jacent à la place:

>>>

>>> import sys
>>> num_bytes_written = sys.stdout.buffer.write(b'\x41\x0a')
A

Cela imprime une lettre majusculeA et un caractère de nouvelle ligne, qui correspondent aux valeurs décimales de 65 et 10 en ASCII. Cependant, ils sont codés en utilisant la notation hexadécimale dans le littéral d'octets.

Notez queprint() n'a aucun contrôle surcharacter encoding. Il incombe au flux de coder correctement les chaînes Unicode reçues en octets. Dans la plupart des cas, vous ne définissez pas l'encodage vous-même, car l'UTF-8 par défaut est ce que vous voulez. Si vous en avez vraiment besoin, peut-être pour les systèmes hérités, vous pouvez utiliser l'argumentencoding deopen():

with open('file.txt', mode='w', encoding='iso-8859-1') as file_object:
    print('über naïve café', file=file_object)

Au lieu d'un fichier réel existant quelque part dans votre système de fichiers, vous pouvez en fournir un faux, qui résiderait dans la mémoire de votre ordinateur. Vous utiliserez cette technique plus tard pour simulerprint() dans les tests unitaires:

>>>

>>> import io
>>> fake_file = io.StringIO()
>>> print('hello world', file=fake_file)
>>> fake_file.getvalue()
'hello world\n'

Si vous en êtes arrivé là, il ne vous reste qu’un seul argument de mot-clé dansprint(), que vous verrez dans la sous-section suivante. C'est probablement le moins utilisé de tous. Néanmoins, il y a des moments où c'est absolument nécessaire.

Mise en mémoire tampon des appels d'impression

Dans la sous-section précédente, vous avez appris queprint() délègue l'impression à un objet de type fichier tel quesys.stdout. Cependant, certains flux tamponnent certaines opérations d'E / S pour améliorer les performances, ce qui peut gêner. Voyons un exemple.

Imaginez que vous écriviez un compte à rebours, qui devrait ajouter le temps restant à la même ligne chaque seconde:

3...2...1...Go!

Votre première tentative peut ressembler à ceci:

import time

num_seconds = 3
for countdown in reversed(range(num_seconds + 1)):
    if countdown > 0:
        print(countdown, end='...')
        time.sleep(1)
    else:
        print('Go!')

Tant que la variablecountdown est supérieure à zéro, le code continue d'ajouter du texte sans une nouvelle ligne de fin, puis se met en veille pendant une seconde. Enfin, lorsque le compte à rebours est terminé, il imprimeGo! et termine la ligne.

De façon inattendue, au lieu de décompter chaque seconde, le programme tourne inutilement au ralenti pendant trois secondes, puis imprime soudainement la ligne entière à la fois:

Terminal with buffered output

C'est parce que le système d'exploitation met en mémoire tampon les écritures suivantes sur la sortie standard dans ce cas. Vous devez savoir qu'il existe trois types de flux en ce qui concerne la mise en mémoire tampon:

  1. Sans tampon

  2. Mise en mémoire tampon de ligne

  3. Tamponné en bloc

Unbuffered est explicite, c'est-à-dire qu'aucune mise en mémoire tampon n'a lieu et que toutes les écritures ont un effet immédiat. Un fluxline-buffered attend avant de déclencher des appels d'E / S jusqu'à ce qu'un saut de ligne apparaisse quelque part dans le tampon, alors qu'un fluxblock-buffered permet simplement au tampon de se remplir jusqu'à une certaine taille quel que soit son contenu. La sortie standard est à la foisline-buffered etblock-buffered, selon l'événement qui vient en premier.

La mise en mémoire tampon permet de réduire le nombre d'appels d'E / S coûteux. Pensez à envoyer des messages sur un réseau à latence élevée, par exemple. Lorsque vous vous connectez à un serveur distant pour exécuter des commandes via le protocole SSH, chacune de vos frappes peut en fait produire un paquet de données individuel, qui est de plusieurs ordres de grandeur plus grand que sa charge utile. Quel frais généraux! Il serait logique d'attendre qu'au moins quelques caractères soient saisis, puis de les envoyer ensemble. C’est là que la mise en mémoire tampon intervient.

D'un autre côté, la mise en mémoire tampon peut parfois avoir des effets indésirables comme vous venez de le voir avec l'exemple du compte à rebours. Pour résoudre ce problème, vous pouvez simplement dire àprint() de vider de force le flux sans attendre un caractère de nouvelle ligne dans le tampon en utilisant son indicateurflush:

print(countdown, end='...', flush=True)

C'est tout. Votre compte à rebours devrait fonctionner comme prévu maintenant, mais ne me croyez pas sur parole. Allez-y et testez-le pour voir la différence.

Toutes nos félicitations! À ce stade, vous avez vu des exemples d'appels deprint() qui couvrent tous ses paramètres. Vous connaissez leur objectif et quand les utiliser. Cependant, comprendre la signature n'est qu'un début. Dans les sections à venir, vous comprendrez pourquoi.

Impression de types de données personnalisés

Jusqu'à présent, vous ne vous occupiez que des types de données intégrés tels que les chaînes et les nombres, mais vous souhaiterez souvent imprimer vos propres types de données abstraits. Voyons différentes façons de les définir.

Pour les objets simples sans aucune logique, dont le but est de transporter des données, vous profiterez généralement denamedtuple, qui est disponible dans la bibliothèque standard. Les tuples nommés ont une représentation textuelle soignée prête à l'emploi:

>>>

>>> from collections import namedtuple
>>> Person = namedtuple('Person', 'name age')
>>> jdoe = Person('John Doe', 42)
>>> print(jdoe)
Person(name='John Doe', age=42)

C’est bien tant que la conservation des données est suffisante, mais pour ajouter des comportements au typePerson, vous devrez éventuellement définir une classe. Jetez un œil à cet exemple:

class Person:
    def __init__(self, name, age):
        self.name, self.age = name, age

Si vous créez maintenant une instance de la classePerson et essayez de l’imprimer, vous obtiendrez cette sortie bizarre, qui est assez différente de l’équivalentnamedtuple:

>>>

>>> jdoe = Person('John Doe', 42)
>>> print(jdoe)
<__main__.Person object at 0x7fcac3fed1d0>

Il s'agit de la représentation par défaut des objets, qui comprend leur adresse en mémoire, le nom de classe correspondant et un module dans lequel ils ont été définis. Vous allez résoudre ce problème dans un instant, mais juste pour mémoire, comme solution de contournement rapide, vous pouvez combinernamedtuple et une classe personnalisée viainheritance:

from collections import namedtuple

class Person(namedtuple('Person', 'name age')):
    pass

Votre classePerson vient de devenir un type spécialisé denamedtuple avec deux attributs que vous pouvez personnaliser.

Note: En Python 3, l'instructionpass peut être remplacée par le littéralellipsis (...) pour indiquer un espace réservé:

def delta(a, b, c):
    ...

Cela empêche l'interpréteur d'éleverIndentationError en raison d'un bloc de code indenté manquant.

C'est mieux qu'un simplenamedtuple, car non seulement vous obtenez une impression correcte gratuitement, mais vous pouvez également ajouter des méthodes et des propriétés personnalisées à la classe. Cependant, il résout un problème tout en en introduisant un autre. N'oubliez pas que les tuples, y compris les tuples nommés, sont immuables en Python, ils ne peuvent donc pas modifier leurs valeurs une fois créés.

Il est vrai que la conception de types de données immuables est souhaitable, mais dans de nombreux cas, vous souhaiterez qu’ils autorisent le changement, vous êtes donc de retour avec des classes régulières.

Note: Suite à d'autres langages et frameworks, Python 3.7 a introduitdata classes, que vous pouvez considérer comme des tuples mutables. De cette façon, vous obtenez le meilleur des deux mondes:

>>>

>>> from dataclasses import dataclass
>>> @dataclass
... class Person:
...     name: str
...     age: int
...
...     def celebrate_birthday(self):
...         self.age += 1
...
>>> jdoe = Person('John Doe', 42)
>>> jdoe.celebrate_birthday()
>>> print(jdoe)
Person(name='John Doe', age=43)

La syntaxe devariable annotations, qui est requise pour spécifier les champs de classe avec leurs types correspondants, a été définie dans Python 3.6.

Dans les sous-sections précédentes, vous savez déjà queprint() appelle implicitement la fonctionstr() intégrée pour convertir ses arguments de position en chaînes. En effet, appelerstr() manuellement sur une instance de la classe normalePerson donne le même résultat que de l'imprimer:

>>>

>>> jdoe = Person('John Doe', 42)
>>> str(jdoe)
'<__main__.Person object at 0x7fcac3fed1d0>'

str(), à son tour, recherche l'un des deuxmagic methods dans le corps de la classe, que vous implémentez généralement. S'il n'en trouve pas, il revient à la représentation par défaut laide. Ces méthodes magiques sont, par ordre de recherche:

  1. def __str__(self)

  2. def __repr__(self)

Le premier est recommandé de renvoyer un texte court et lisible par l'homme, qui comprend des informations sur les attributs les plus pertinents. Après tout, vous ne souhaitez pas exposer des données sensibles, telles que les mots de passe des utilisateurs, lors de l'impression d'objets.

Cependant, l'autre doit fournir des informations complètes sur un objet, pour permettre de restaurer son état à partir d'une chaîne. Idéalement, il devrait renvoyer du code Python valide, afin que vous puissiez le passer directement àeval():

>>>

>>> repr(jdoe)
"Person(name='John Doe', age=42)"
>>> type(eval(repr(jdoe)))

Notez l’utilisation d’une autre fonction intégrée,repr(), qui essaie toujours d’appeler.__repr__() dans un objet, mais revient à la représentation par défaut s’il ne trouve pas cette méthode.

Note: Même siprint() lui-même utilisestr() pour la conversion de type, certains types de données composés délèguent cet appel àrepr() sur leurs membres. Cela arrive par exemple aux listes et aux tuples.

Considérez cette classe avec les deux méthodes magiques, qui renvoient des représentations de chaîne alternatives du même objet:

class User:
    def __init__(self, login, password):
        self.login = login
        self.password = password

    def __str__(self):
        return self.login

    def __repr__(self):
        return f"User('{self.login}', '{self.password}')"

Si vous imprimez un seul objet de la classeUser, vous ne verrez pas le mot de passe, carprint(user) appellerastr(user), ce qui finira par appeleruser.__str__():

>>>

>>> user = User('jdoe', 's3cret')
>>> print(user)
jdoe

Cependant, si vous mettez la même variableuser dans une liste en la mettant entre crochets, le mot de passe deviendra clairement visible:

>>>

>>> print([user])
[User('jdoe', 's3cret')]

En effet, les séquences, telles que les listes et les tuples, implémentent leur méthode.__str__() afin que tous leurs éléments soient d'abord convertis avecrepr().

Python vous donne beaucoup de liberté lorsqu'il s'agit de définir vos propres types de données si aucun des types intégrés ne répond à vos besoins. Certains d'entre eux, tels que les tuples nommés et les classes de données, offrent des représentations de chaînes qui semblent bonnes sans nécessiter de travail de votre part. Néanmoins, pour plus de flexibilité, vous devrez définir une classe et remplacer ses méthodes magiques décrites ci-dessus.

Comprendre l'impression Python

Vous savez quehow utilise assez bienprint() à ce stade, mais connaîtrewhat cela vous permettra de l'utiliser encore plus efficacement et consciemment. Après avoir lu cette section, vous comprendrez comment l'impression en Python s'est améliorée au fil des ans.

L'impression est une fonction dans Python 3

Vous avez vu queprint() est une fonction en Python 3. Plus précisément, il s'agit d'une fonction intégrée, ce qui signifie que vous n'avez pas besoin de l'importer de n'importe où:

>>>

>>> print

Il est toujours disponible dans l'espace de noms global pour que vous puissiez l'appeler directement, mais vous pouvez également y accéder via un module à partir de la bibliothèque standard:

>>>

>>> import builtins
>>> builtins.print

De cette façon, vous pouvez éviter les collisions de noms avec des fonctions personnalisées. Supposons que vous vouliezredefineprint() pour qu'il n'ajoute pas de nouvelle ligne à la fin. En même temps, vous vouliez renommer la fonction d'origine en quelque chose commeprintln():

>>>

>>> import builtins
>>> println = builtins.print
>>> def print(*args, **kwargs):
...     builtins.print(*args, **kwargs, end='')
...
>>> println('hello')
hello
>>> print('hello\n')
hello

Vous disposez maintenant de deux fonctions d'impression distinctes, tout comme dans le langage de programmation Java. Vous définirez également les fonctionsprint() personnalisées dans lesmocking section ultérieurement. Notez également que vous ne pourrez pas écraserprint() en premier lieu s'il ne s'agissait pas d'une fonction.

D'un autre côté,print() n'est pas une fonction au sens mathématique, car il ne renvoie aucune valeur significative autre que lesNone implicites:

>>>

>>> value = print('hello world')
hello world
>>> print(value)
None

De telles fonctions sont, en fait, des procédures ou des sous-programmes que vous appelez pour obtenir une sorte d'effet secondaire, qui est finalement un changement d'état global. Dans le cas deprint(), cet effet secondaire affiche un message sur la sortie standard ou écrit dans un fichier.

Puisqueprint() est une fonction, elle a une signature bien définie avec des attributs connus. Vous pouvez trouver rapidement sesdocumentation en utilisant l'éditeur de votre choix, sans avoir à vous souvenir d'une syntaxe étrange pour effectuer une certaine tâche.

De plus, les fonctions sont plus faciles pourextend. L'ajout d'une nouvelle fonctionnalité à une fonction est aussi simple que l'ajout d'un autre argument de mot clé, tandis que changer la langue pour prendre en charge cette nouvelle fonctionnalité est beaucoup plus lourd. Pensez à la redirection de flux ou au vidage de tampon, par exemple.

Un autre avantage deprint() étant une fonction estcomposability. Les fonctions sont appeléesfirst-class objects oufirst-class citizens en Python, ce qui est une manière sophistiquée de dire que ce sont des valeurs comme des chaînes ou des nombres. De cette façon, vous pouvez affecter une fonction à une variable, la transmettre à une autre fonction ou même la renvoyer l'une à l'autre. print() n'est pas différent à cet égard. Par exemple, vous pouvez en profiter pour l'injection de dépendances:

def download(url, log=print):
    log(f'Downloading {url}')
    # ...

def custom_print(*args):
    pass  # Do not print anything

download('/js/app.js', log=custom_print)

Ici, le paramètrelog vous permet d'injecter une fonction de rappel, qui par défaut estprint() mais peut être n'importe quel appelable. Dans cet exemple, l'impression est complètement désactivée en remplaçantprint() par une fonction factice qui ne fait rien.

Note: Undependency est tout morceau de code requis par un autre bit de code.

Dependency injection est une technique utilisée dans la conception de code pour le rendre plus testable, réutilisable et ouvert à l'extension. Vous pouvez y parvenir en faisant référence aux dépendances indirectement via des interfaces abstraites et en les fournissant de manièrepush plutôt quepull.

Il y a une explication amusante de l'injection de dépendance circulant sur Internet:

Injection de dépendance pour les enfants de cinq ans

Lorsque vous sortez du réfrigérateur par vous-même, vous pouvez causer des problèmes. Vous pourriez laisser la porte ouverte, vous pourriez avoir quelque chose que maman ou papa ne veut pas que vous ayez. Vous cherchez peut-être même quelque chose que nous n'avons même pas ou qui a expiré.

Ce que vous devriez faire, c'est énoncer un besoin: «J'ai besoin de quelque chose à boire avec le déjeuner», puis nous nous assurerons que vous avez quelque chose lorsque vous vous asseyez pour manger.

-John Munsch, 28 October 2009. (Source)

La composition vous permet de combiner quelques fonctions en une nouvelle du même type. Voyons cela en action en spécifiant une fonctionerror() personnalisée qui imprime dans le flux d'erreur standard et préfixe tous les messages avec un niveau de journal donné:

>>>

>>> from functools import partial
>>> import sys
>>> redirect = lambda function, stream: partial(function, file=stream)
>>> prefix = lambda function, prefix: partial(function, prefix)
>>> error = prefix(redirect(print, sys.stderr), '[ERROR]')
>>> error('Something went wrong')
[ERROR] Something went wrong

Cette fonction personnalisée utilisepartial functions pour obtenir l'effet souhaité. Il s'agit d'un concept avancé emprunté au paradigmefunctional programming, vous n'avez donc pas besoin d'aller trop loin dans ce sujet pour le moment. Cependant, si vous êtes intéressé par ce sujet, je vous recommande de jeter un œil au modulefunctools.

Contrairement aux instructions, les fonctions sont des valeurs. Cela signifie que vous pouvez les mélanger avecexpressions, en particulier,lambda expressions. Au lieu de définir une fonction complète pour remplacerprint() par, vous pouvez créer une expression lambda anonyme qui l'appelle:

>>>

>>> download('/js/app.js', lambda msg: print('[INFO]', msg))
[INFO] Downloading /js/app.js

Cependant, comme une expression lambda est définie sur place, il n'y a aucun moyen de s'y référer ailleurs dans le code.

Note: En Python, vous ne pouvez pas placer d'instructions, telles que des affectations, des instructions conditionnelles, des boucles, etc., dans unanonymous lambda function. Ce doit être une seule expression!

Un autre type d'expression est une expression conditionnelle ternaire:

>>>

>>> user = 'jdoe'
>>> print('Hi!') if user is None else print(f'Hi, {user}.')
Hi, jdoe.

Python a à la foisconditional statements etconditional expressions. Ce dernier est évalué en une valeur unique qui peut être affectée à une variable ou passée à une fonction. Dans l'exemple ci-dessus, vous êtes intéressé par l'effet secondaire plutôt que par la valeur, qui équivaut àNone, donc vous l'ignorez simplement.

Comme vous pouvez le voir, les fonctions permettent une solution élégante et extensible, cohérente avec le reste du langage. Dans la sous-section suivante, vous découvrirez comment le fait de ne pas avoirprint() en tant que fonction a causé beaucoup de maux de tête.

Imprimer était une déclaration en Python 2

Unstatement est une instruction qui peut évoquer un effet secondaire lors de son exécution mais qui ne s'évalue jamais à une valeur. En d'autres termes, vous ne pourriez pas imprimer une instruction ou l'attribuer à une variable comme celle-ci:

result = print 'hello world'

C'est une erreur de syntaxe dans Python 2.

Voici quelques autres exemples d'instructions en Python:

  • affectation: =

  • conditionnel: if

  • boucle: while

  • assertion:assert

Note: Python 3.8 apporte unwalrus operator (:=) controversé, qui est unassignment expression. Avec lui, vous pouvez évaluer une expression et affecter le résultat à une variable en même temps, même dans une autre expression!

Jetez un œil à cet exemple, qui appelle une fois une fonction coûteuse, puis réutilise le résultat pour un calcul ultérieur:

# Python 3.8+
values = [y := f(x), y**2, y**3]

Ceci est utile pour simplifier le code sans perdre son efficacité. En règle générale, le code performant a tendance à être plus détaillé:

y = f(x)
values = [y, y**2, y**3]

La controverse derrière ce nouveau morceau de syntaxe a provoqué beaucoup d'arguments. Une abondance de commentaires négatifs et de débats passionnés a finalement conduit Guido van Rossum à se retirer de la positionBenevolent Dictator For Life ou BDFL.

Les instructions sont généralement composées de mots-clés réservés tels queif,for ouprint qui ont une signification fixe dans la langue. Vous ne pouvez pas les utiliser pour nommer vos variables ou autres symboles. C'est pourquoi il n'est pas possible de redéfinir ou de se moquer de l'instructionprint dans Python 2. Vous êtes coincé avec ce que vous obtenez.

De plus, vous ne pouvez pas imprimer à partir de fonctions anonymes, car les instructions ne sont pas acceptées dans les expressions lambda:

>>>

>>> lambda: print 'hello world'
  File "", line 1
    lambda: print 'hello world'
                ^
SyntaxError: invalid syntax

La syntaxe de l'instructionprint est ambiguë. Parfois, vous pouvez ajouter des parenthèses autour du message, et elles sont complètement facultatives:

>>>

>>> print 'Please wait...'
Please wait...
>>> print('Please wait...')
Please wait...

À d'autres moments, ils modifient la façon dont le message est imprimé:

>>>

>>> print 'My name is', 'John'
My name is John
>>> print('My name is', 'John')
('My name is', 'John')

La concaténation de chaînes peut augmenter unTypeError en raison de types incompatibles, que vous devez gérer manuellement, par exemple:

>>>

>>> values = ['jdoe', 'is', 42, 'years old']
>>> print ' '.join(map(str, values))
jdoe is 42 years old

Comparez cela avec un code similaire dans Python 3, qui exploite le décompression de séquence:

>>>

>>> values = ['jdoe', 'is', 42, 'years old']
>>> print(*values)  # Python 3
jdoe is 42 years old

Il n’existe aucun argument de mot clé pour les tâches courantes telles que le vidage du tampon ou la redirection de flux. Vous devez vous souvenir de la syntaxe excentrique à la place. Même la fonction intégréehelp() n’est pas si utile en ce qui concerne l’instructionprint:

>>>

>>> help(print)
  File "", line 1
    help(print)
             ^
SyntaxError: invalid syntax

La suppression de la nouvelle ligne ne fonctionne pas correctement, car elle ajoute un espace indésirable. Vous ne pouvez pas composer plusieurs instructionsprint ensemble, et, en plus de cela, vous devez être très diligent sur le codage des caractères.

La liste des problèmes s'allonge encore et encore. Si vous êtes curieux, vous pouvez revenir auxprevious section et rechercher des explications plus détaillées sur la syntaxe de Python 2.

Cependant, vous pouvez atténuer certains de ces problèmes avec une approche beaucoup plus simple. Il s'avère que la fonctionprint() a été rétroportée pour faciliter la migration vers Python 3. Vous pouvez l'importer à partir d'un module spécial__future__, qui expose une sélection de fonctionnalités de langage publiées dans les versions ultérieures de Python.

Note: Vous pouvez importer des fonctions futures ainsi que des constructions de langage intégrées telles que l'instructionwith.

Pour savoir exactement quelles fonctionnalités sont à votre disposition, inspectez le module:

>>>

>>> import __future__
>>> __future__.all_feature_names
['nested_scopes',
 'generators',
 'division',
 'absolute_import',
 'with_statement',
 'print_function',
 'unicode_literals']

Vous pouvez également appelerdir(__future__), mais cela montrerait beaucoup de détails internes inintéressants du module.

Pour activer la fonctionprint() dans Python 2, vous devez ajouter cette instruction d'importation au début de votre code source:

from __future__ import print_function

Désormais, l'instructionprint n'est plus disponible, mais vous avez la fonctionprint() à votre disposition. Notez qu'il ne s'agit pas de la même fonction que celle de Python 3, car il manque l'argument de mot-cléflush, mais le reste des arguments sont les mêmes.

À part cela, cela ne vous évite pas de gérer correctement les encodages de caractères.

Voici un exemple d’appel de la fonctionprint() dans Python 2:

>>>

>>> from __future__ import print_function
>>> import sys
>>> print('I am a function in Python', sys.version_info.major)
I am a function in Python 2

Vous avez maintenant une idée de l'évolution de l'impression en Python et, plus important encore, vous comprenez pourquoi ces changements incompatibles en amont étaient nécessaires. Le savoir vous aidera sûrement à devenir un meilleur programmeur Python.

Impression avec style

Si vous pensiez que l'impression consistait uniquement à éclairer les pixels sur l'écran, alors techniquement, vous auriez raison. Cependant, il existe des moyens de le rendre cool. Dans cette section, vous découvrirez comment formater des structures de données complexes, ajouter des couleurs et d'autres décorations, créer des interfaces, utiliser des animations et même jouer des sons avec du texte!

Jolies impressions de structures de données imbriquées

Les langages informatiques vous permettent de représenter des données ainsi que du code exécutable de manière structurée. Contrairement à Python, cependant, la plupart des langues vous offrent une grande liberté dans l'utilisation des espaces et du formatage. Cela peut être utile, par exemple en compression, mais cela conduit parfois à un code moins lisible.

La jolie impression consiste à rendre une donnée ou un code plus attrayant pour l'œil humain afin de le comprendre plus facilement. Pour ce faire, indenter certaines lignes, insérer des nouvelles lignes, réorganiser les éléments, etc.

Python est livré avec le modulepprint dans sa bibliothèque standard, ce qui vous aidera à imprimer de jolies structures de données volumineuses qui ne tiennent pas sur une seule ligne. Parce qu'il imprime de manière plus conviviale, de nombreux outilsREPL populaires, y comprisJupyterLab and IPython, l'utilisent par défaut à la place de la fonction normaleprint().

Note: Pour activer la jolie impression dans IPython, exécutez la commande suivante:

>>>

In [1]: %pprint
Pretty printing has been turned OFF
In [2]: %pprint
Pretty printing has been turned ON

Ceci est un exemple deMagic dans IPython. Il existe de nombreuses commandes intégrées qui commencent par un signe de pourcentage (%), mais vous pouvez en trouver plus surPyPI, ou même créer les vôtres.

Si vous ne vous souciez pas de ne pas avoir accès à la fonction d'origineprint(), vous pouvez la remplacer parpprint() dans votre code en utilisant le changement de nom d'importation:

>>>

>>> from pprint import pprint as print
>>> print

Personnellement, j'aime avoir les deux fonctions à portée de main, donc je préfère utiliser quelque chose commepp comme alias court:

from pprint import pprint as pp

À première vue, il n'y a pratiquement pas de différence entre les deux fonctions, et dans certains cas, il n'y en a pratiquement aucune:

>>>

>>> print(42)
42
>>> pp(42)
42
>>> print('hello')
hello
>>> pp('hello')
'hello'  # Did you spot the difference?

C'est parce quepprint() appellerepr() au lieu desstr() habituels pour la conversion de type, de sorte que vous pouvez évaluer sa sortie en tant que code Python si vous le souhaitez. Les différences deviennent apparentes lorsque vous commencez à lui fournir des structures de données plus complexes:

>>>

>>> data = {'powers': [x**10 for x in range(10)]}
>>> pp(data)
{'powers': [0,
            1,
            1024,
            59049,
            1048576,
            9765625,
            60466176,
            282475249,
            1073741824,
            3486784401]}

La fonction applique une mise en forme raisonnable pour améliorer la lisibilité, mais vous pouvez la personnaliser encore plus avec quelques paramètres. Par exemple, vous pouvez limiter une hiérarchie profondément imbriquée en affichant des points de suspension sous un niveau donné:

>>>

>>> cities = {'USA': {'Texas': {'Dallas': ['Irving']}}}
>>> pp(cities, depth=3)
{'USA': {'Texas': {'Dallas': [...]}}}

Lesprint() ordinaires utilisent également des ellipses, mais pour afficher des structures de données récursives, qui forment un cycle, afin d'éviter une erreur de débordement de pile:

>>>

>>> items = [1, 2, 3]
>>> items.append(items)
>>> print(items)
[1, 2, 3, [...]]

Cependant,pprint() est plus explicite à ce sujet en incluant l'identité unique d'un objet auto-référencé:

>>>

>>> pp(items)
[1, 2, 3, ]
>>> id(items)
140635757287688

Le dernier élément de la liste est le même objet que la liste entière.

Note: Les ensembles de données récursifs ou très volumineux peuvent également être traités en utilisant le modulereprlib:

>>>

>>> import reprlib
>>> reprlib.repr([x**10 for x in range(10)])
'[0, 1, 1024, 59049, 1048576, 9765625, ...]'

Ce module prend en charge la plupart des types intégrés et est utilisé par le débogueur Python.

pprint() trie automatiquement les clés du dictionnaire pour vous avant l'impression, ce qui permet une comparaison cohérente. Lorsque vous comparez des chaînes, vous ne vous souciez souvent pas d'un ordre particulier d'attributs sérialisés. Quoi qu'il en soit, il est toujours préférable de comparer les dictionnaires réels avant la sérialisation.

Les dictionnaires représentent souventJSON data, qui est largement utilisé sur Internet. Pour sérialiser correctement un dictionnaire dans une chaîne au format JSON valide, vous pouvez tirer parti du modulejson. Il a également de jolies capacités d'impression:

>>>

>>> import json
>>> data = {'username': 'jdoe', 'password': 's3cret'}
>>> ugly = json.dumps(data)
>>> pretty = json.dumps(data, indent=4, sort_keys=True)
>>> print(ugly)
{"username": "jdoe", "password": "s3cret"}
>>> print(pretty)
{
    "password": "s3cret",
    "username": "jdoe"
}

Notez cependant que vous devez gérer l'impression vous-même, car ce n'est pas quelque chose que vous souhaitez généralement faire. De même, le modulepprint a une fonctionpformat() supplémentaire qui renvoie une chaîne, au cas où vous auriez à faire autre chose que de l'imprimer.

Étonnamment, la signature depprint() ne ressemble en rien à celle de la fonctionprint(). Vous ne pouvez même pas passer plus d'un argument positionnel, ce qui montre à quel point il se concentre sur l'impression des structures de données.

Ajout de couleurs avec des séquences d'échappement ANSI

À mesure que les ordinateurs personnels devenaient plus sophistiqués, ils avaient de meilleurs graphismes et pouvaient afficher plus de couleurs. Cependant, différents fournisseurs avaient leur propre idée de la conception de l'API pour la contrôler. Cela a changé il y a quelques décennies lorsque les gens de l'American National Standards Institute ont décidé de l'unifier en définissantANSI escape codes.

La plupart des émulateurs de terminaux d'aujourd'hui prennent en charge cette norme dans une certaine mesure. Jusqu'à récemment, le système d'exploitation Windows était une exception notable. Par conséquent, si vous voulez la meilleure portabilité, utilisez la bibliothèquecolorama en Python. Il traduit les codes ANSI à leurs homologues appropriés dans Windows tout en les conservant intacts dans d'autres systèmes d'exploitation.

Pour vérifier si votre terminal comprend un sous-ensemble des séquences d'échappement ANSI, par exemple, liées aux couleurs, vous pouvez essayer d'utiliser la commande suivante:

$ tput colors

Mon terminal par défaut sur Linux dit qu'il peut afficher 256 couleurs distinctes, tandis que xterm m'en donne seulement 8. La commande retournerait un nombre négatif si les couleurs n'étaient pas prises en charge.

Les séquences d'échappement ANSI sont comme un langage de balisage pour le terminal. En HTML, vous travaillez avec des balises, telles que<b> ou<i>, pour modifier l'apparence des éléments dans le document. Ces balises sont mélangées à votre contenu, mais elles ne sont pas visibles elles-mêmes. De même, les codes d'échappement ne s'affichent pas dans le terminal tant qu'il les reconnaît. Sinon, ils apparaîtront sous la forme littérale comme si vous consultiez la source d'un site Web.

Comme son nom l'indique, une séquence doit commencer par le caractère non imprimable[.kbd .key-escape]#Esc #, dont la valeur ASCII est 27, parfois désigné par0x1b en hexadécimal ou033 en octal. Vous pouvez utiliser des littéraux numériques Python pour vérifier rapidement qu'il s'agit bien du même numéro:

>>>

>>> 27 == 0x1b == 0o33
True

De plus, vous pouvez l'obtenir avec la séquence d'échappement\e dans le shell:

$ echo -e "\e"

Les séquences d'échappement ANSI les plus courantes prennent la forme suivante:

Élément La description Exemple

[.kbd .key-escape]#Esc #

caractère d'échappement non imprimable

\033

[

support carré ouvrant

[

code numérique

un ou plusieurs nombres séparés par;

0

code de caractère

lettre majuscule ou minuscule

m

Lesnumeric code peuvent être un ou plusieurs nombres séparés par un point-virgule, tandis que lescharacter code ne sont qu'une lettre. Leur signification spécifique est définie par la norme ANSI. Par exemple, pour réinitialiser tout le formatage, vous devez taper l'une des commandes suivantes, qui utilisent le code zéro et la lettrem:

$ echo -e "\e[0m"
$ echo -e "\x1b[0m"
$ echo -e "\033[0m"

À l'autre extrémité du spectre, vous avez des valeurs de code composées. Pour définir l'avant-plan et l'arrière-plan avec des canaux RVB, étant donné que votre terminal prend en charge une profondeur de 24 bits, vous pouvez fournir plusieurs nombres:

$ echo -e "\e[38;2;0;0;0m\e[48;2;255;255;255mBlack on white\e[0m"

Ce n'est pas seulement la couleur du texte que vous pouvez définir avec les codes d'échappement ANSI. Vous pouvez, par exemple, effacer et faire défiler la fenêtre du terminal, modifier son arrière-plan, déplacer le curseur, faire clignoter le texte ou le décorer avec un soulignement.

En Python, vous écririez probablement une fonction d'assistance pour permettre d'encapsuler des codes arbitraires dans une séquence:

>>>

>>> def esc(code):
...     return f'\033[{code}m'
...
>>> print(esc('31;1;4') + 'really' + esc(0) + ' important')

Cela ferait apparaître le motreally dans une police rouge, grasse et soulignée:

Text formatted with ANSI escape codes

Cependant, il existe des abstractions de plus haut niveau sur les codes d'échappement ANSI, comme la bibliothèquecolorama mentionnée, ainsi que des outils pour créer des interfaces utilisateur dans la console.

Création d'interfaces utilisateur de console

Bien que jouer avec les codes d'échappement ANSI soit indéniablement très amusant, dans le monde réel, vous préférez avoir des blocs de construction plus abstraits pour créer une interface utilisateur. Il existe quelques bibliothèques qui offrent un niveau de contrôle aussi élevé sur le terminal, maiscurses semble être le choix le plus populaire.

Note: Pour utiliser la bibliothèquecurses sous Windows, vous devez installer un package tiers:

C:\> pip install windows-curses

C'est parce quecurses n'est pas disponible dans la bibliothèque standard de la distribution Python pour Windows.

Principalement, il vous permet de penser en termes de widgets graphiques indépendants au lieu d'une goutte de texte. De plus, vous avez beaucoup de liberté pour exprimer votre artiste intérieur, car c'est vraiment comme peindre une toile vierge. La bibliothèque cache la complexité d'avoir à traiter avec différents terminaux. En dehors de cela, il prend en charge les événements de clavier, ce qui pourrait être utile pour écrire des jeux vidéo.

Que diriez-vous de faire un jeu de serpent rétro? Créons un simulateur de serpent Python:

The retro snake game built with curses library

Tout d'abord, vous devez importer le modulecurses. Puisqu'il modifie l'état d'un terminal en cours d'exécution, il est important de gérer les erreurs et de restaurer gracieusement l'état précédent. Vous pouvez le faire manuellement, mais la bibliothèque est livrée avec un wrapper pratique pour votre fonction principale:

import curses

def main(screen):
    pass

if __name__ == '__main__':
    curses.wrapper(main)

Notez que la fonction doit accepter une référence à l’objet écran, également appeléstdscr, que vous utiliserez plus tard pour une configuration supplémentaire.

Si vous exécutez ce programme maintenant, vous ne verrez aucun effet, car il se termine immédiatement. Cependant, vous pouvez ajouter un petit délai pour avoir un aperçu:

import time, curses

def main(screen):
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

Cette fois, l'écran est devenu complètement vide pendant une seconde, mais le curseur clignotait toujours. Pour le masquer, il suffit d'appeler l'une des fonctions de configuration définies dans le module:

import time, curses

def main(screen):
    curses.curs_set(0)  # Hide the cursor
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

Définissons le serpent comme une liste de points en coordonnées d'écran:

snake = [(0, i) for i in reversed(range(20))]

La tête du serpent est toujours le premier élément de la liste, tandis que la queue est le dernier. La forme initiale du serpent est horizontale, partant du coin supérieur gauche de l'écran et orientée vers la droite. Alors que sa coordonnée y reste à zéro, sa coordonnée x diminue de la tête à la queue.

Pour dessiner le serpent, vous commencerez par la tête, puis suivrez avec les segments restants. Chaque segment porte les coordonnées(y, x), vous pouvez donc les décompresser:

# Draw the snake
screen.addstr(*snake[0], '@')
for segment in snake[1:]:
    screen.addstr(*segment, '*')

Encore une fois, si vous exécutez ce code maintenant, il n'affichera rien, car vous devez explicitement actualiser l'écran par la suite:

import time, curses

def main(screen):
    curses.curs_set(0)  # Hide the cursor

    snake = [(0, i) for i in reversed(range(20))]

    # Draw the snake
    screen.addstr(*snake[0], '@')
    for segment in snake[1:]:
        screen.addstr(*segment, '*')

    screen.refresh()
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

Vous souhaitez déplacer le serpent dans l'une des quatre directions, qui peuvent être définies comme des vecteurs. Finalement, la direction changera en réponse à une touche fléchée, vous pouvez donc la raccorder aux codes clés de la bibliothèque:

directions = {
    curses.KEY_UP: (-1, 0),
    curses.KEY_DOWN: (1, 0),
    curses.KEY_LEFT: (0, -1),
    curses.KEY_RIGHT: (0, 1),
}

direction = directions[curses.KEY_RIGHT]

Comment se déplace un serpent? Il s'avère que seule sa tête se déplace réellement vers un nouvel emplacement, tandis que tous les autres segments se déplacent vers elle. À chaque étape, presque tous les segments restent les mêmes, à l'exception de la tête et de la queue. En supposant que le serpent ne grandit pas, vous pouvez retirer la queue et insérer une nouvelle tête au début de la liste:

# Move the snake
snake.pop()
snake.insert(0, tuple(map(sum, zip(snake[0], direction))))

Pour obtenir les nouvelles coordonnées de la tête, vous devez lui ajouter le vecteur de direction. Cependant, l'ajout de tuples en Python entraîne un plus grand tuple au lieu de la somme algébrique des composantes vectorielles correspondantes. Une façon de résoudre ce problème consiste à utiliser les fonctions intégréeszip(),sum() etmap().

La direction changera en appuyant sur une touche, vous devez donc appeler.getch() pour obtenir le code de la touche enfoncée. Cependant, si la touche enfoncée ne correspond pas aux touches fléchées définies précédemment comme touches de dictionnaire, la direction ne changera pas:

# Change direction on arrow keystroke
direction = directions.get(screen.getch(), direction)

Par défaut, cependant,.getch() est un appel bloquant qui empêcherait le serpent de bouger à moins qu'il y ait une frappe. Par conséquent, vous devez rendre l'appel non bloquant en ajoutant une autre configuration:

def main(screen):
    curses.curs_set(0)    # Hide the cursor
    screen.nodelay(True)  # Don't block I/O calls

Vous avez presque terminé, mais il ne reste qu’une dernière chose. Si vous bouclez maintenant ce code, le serpent semblera grandir au lieu de se déplacer. C'est parce que vous devez effacer l'écran explicitement avant chaque itération.

Enfin, c'est tout ce dont vous avez besoin pour jouer au jeu de serpent en Python:

import time, curses

def main(screen):
    curses.curs_set(0)    # Hide the cursor
    screen.nodelay(True)  # Don't block I/O calls

    directions = {
        curses.KEY_UP: (-1, 0),
        curses.KEY_DOWN: (1, 0),
        curses.KEY_LEFT: (0, -1),
        curses.KEY_RIGHT: (0, 1),
    }

    direction = directions[curses.KEY_RIGHT]
    snake = [(0, i) for i in reversed(range(20))]

    while True:
        screen.erase()

        # Draw the snake
        screen.addstr(*snake[0], '@')
        for segment in snake[1:]:
            screen.addstr(*segment, '*')

        # Move the snake
        snake.pop()
        snake.insert(0, tuple(map(sum, zip(snake[0], direction))))

        # Change direction on arrow keystroke
        direction = directions.get(screen.getch(), direction)

        screen.refresh()
        time.sleep(0.1)

if __name__ == '__main__':
    curses.wrapper(main)

Ceci ne fait qu'effleurer la surface des possibilités qu'ouvre le modulecurses. Vous pouvez l'utiliser pour le développement de jeux comme celui-ci ou pour des applications plus commerciales.

Vivre avec des animations sympas

Les animations peuvent non seulement rendre l'interface utilisateur plus attrayante pour les yeux, mais elles améliorent également l'expérience utilisateur globale. Lorsque vous fournissez des commentaires précoces à l'utilisateur, par exemple, il saura si votre programme fonctionne toujours ou s'il est temps de le supprimer.

Pour animer du texte dans le terminal, vous devez pouvoir déplacer librement le curseur. Vous pouvez le faire avec l'un des outils mentionnés précédemment, à savoir les codes d'échappement ANSI ou la bibliothèquecurses. Cependant, je voudrais vous montrer une manière encore plus simple.

Si l'animation peut être limitée à une seule ligne de texte, vous pourriez être intéressé par deux séquences de caractères d'échappement spéciales:

  • Retour chariot: \r

  • Retour arrière: \b

Le premier déplace le curseur au début de la ligne, tandis que le second déplace un seul caractère vers la gauche. Ils fonctionnent tous les deux de manière non destructrice sans écraser le texte déjà écrit.

Jetons un œil à quelques exemples.

Vous souhaiterez souvent afficher une sorte despinning wheel pour indiquer un travail en cours sans savoir exactement combien de temps il reste à terminer:

Indefinite animation in the terminal

De nombreux outils de ligne de commande utilisent cette astuce lors du téléchargement de données sur le réseau. Vous pouvez créer une animation stop motion vraiment simple à partir d'une séquence de caractères qui se déroulera de manière circulaire:

from itertools import cycle
from time import sleep

for frame in cycle(r'-\|/-\|/'):
    print('\r', frame, sep='', end='', flush=True)
    sleep(0.2)

La boucle obtient le caractère suivant à imprimer, puis déplace le curseur au début de la ligne et écrase tout ce qu'il y avait avant sans ajouter de nouvelle ligne. Vous ne voulez pas d'espace supplémentaire entre les arguments positionnels, donc l'argument séparateur doit être vide. Notez également l'utilisation des chaînes brutes de Python en raison des caractères de barre oblique inverse présents dans le littéral.

Lorsque vous connaissez le temps restant ou le pourcentage d’achèvement des tâches, vous pouvez afficher une barre de progression animée:

Progress bar animation in the terminal

Tout d'abord, vous devez calculer le nombre de hashtags à afficher et le nombre d'espaces vides à insérer. Ensuite, vous effacez la ligne et créez la barre à partir de zéro:

from time import sleep

def progress(percent=0, width=30):
    left = width * percent // 100
    right = width - left
    print('\r[', '#' * left, ' ' * right, ']',
          f' {percent:.0f}%',
          sep='', end='', flush=True)

for i in range(101):
    progress(i)
    sleep(0.1)

Comme précédemment, chaque demande de mise à jour repeint la ligne entière.

Note: Il existe une bibliothèqueprogressbar2 riche en fonctionnalités, ainsi que quelques autres outils similaires, qui peuvent montrer les progrès de manière beaucoup plus complète.

Création de sons avec impression

Si vous êtes assez vieux pour vous souvenir des ordinateurs équipés d'un haut-parleur de PC, vous devez également vous souvenir de leur sonbeep distinctif, souvent utilisé pour indiquer des problèmes matériels. Ils pouvaient à peine faire plus de bruit que ça, mais les jeux vidéo semblaient tellement mieux avec ça.

Aujourd'hui, vous pouvez toujours profiter de ce petit haut-parleur, mais il est probable que votre ordinateur portable n'en soit pas équipé. Dans un tel cas, vous pouvez activer l'émulationterminal bell dans votre shell, de sorte qu'un son d'avertissement système soit émis à la place.

Allez-y et tapez cette commande pour voir si votre terminal peut jouer un son:

$ echo -e "\a"

Cela afficherait normalement du texte, mais l'indicateur-e permet l'interprétation des échappements de barre oblique inverse. Comme vous pouvez le voir, il existe une séquence d'échappement dédiée, qui signifie «alerte», qui génère unbell character spécial. Certains terminaux émettent un son chaque fois qu'ils le voient.

De même, vous pouvez imprimer ce caractère en Python. Peut-être en boucle pour former une sorte de mélodie. Bien qu'il ne s'agisse que d'une seule note, vous pouvez toujours faire varier la durée des pauses entre des instances consécutives. Cela semble être un jouet parfait pour la lecture du code Morse!

Les règles sont les suivantes:

  • Les lettres sont codées avec une séquence de symbolesdot (·) etdash (-).

  • Undot est une unité de temps.

  • Undash correspond à trois unités de temps.

  • Lessymbols individuels d'une lettre sont espacés d'une unité de temps.

  • Les symboles de deuxlettersadjacents sont espacés de trois unités de temps.

  • Les symboles de deuxwordsadjacents sont espacés de sept unités de temps.

Selon ces règles, vous pouvez «imprimer» un signal SOS indéfiniment de la manière suivante:

while True:
    dot()
    symbol_space()
    dot()
    symbol_space()
    dot()
    letter_space()
    dash()
    symbol_space()
    dash()
    symbol_space()
    dash()
    letter_space()
    dot()
    symbol_space()
    dot()
    symbol_space()
    dot()
    word_space()

En Python, vous pouvez l'implémenter en seulement dix lignes de code:

from time import sleep

speed = 0.1

def signal(duration, symbol):
    sleep(duration)
    print(symbol, end='', flush=True)

dot = lambda: signal(speed, '·\a')
dash = lambda: signal(3*speed, '−\a')
symbol_space = lambda: signal(speed, '')
letter_space = lambda: signal(3*speed, '')
word_space = lambda: signal(7*speed, ' ')

Peut-être pourriez-vous même aller plus loin et créer un outil en ligne de commande pour traduire du texte en code Morse? Quoi qu'il en soit, j'espère que vous vous amusez avec ça!

Impression Python moqueuse dans les tests unitaires

De nos jours, on s'attend à ce que vous expédiez un code qui répond à des normes de qualité élevées. Si vous aspirez à devenir professionnel, vous devez apprendrehow to test votre code.

Les tests de logiciels sont particulièrement importants dans les langages typés dynamiquement, tels que Python, qui n'ont pas de compilateur pour vous avertir des erreurs évidentes. Les défauts peuvent se frayer un chemin vers l'environnement de production et rester dormants pendant une longue période, jusqu'au jour où une branche de code sera finalement exécutée.

Bien sûr, vous disposez delinters,type checkers et d'autres outils d'analyse de code statique pour vous aider. Mais ils ne vous diront pas si votre programme fait ce qu'il est censé faire au niveau de l'entreprise.

Alors, devriez-vous testerprint()? No. Après tout, c'est une fonction intégrée qui doit avoir déjà subi une suite complète de tests. Ce que vous voulez tester, cependant, c'est si votre code appelleprint() au bon moment avec les paramètres attendus. C'est ce qu'on appelle unbehavior.

Vous pouvez tester les comportements par des objets ou des fonctions réels democking. Dans ce cas, vous voulez vous moquer deprint() pour enregistrer et vérifier ses appels.

Note: Vous avez peut-être entendu les termes:dummy,fake,stub,spy oumock utilisés de manière interchangeable. Certaines personnes font une distinction entre elles, d’autres pas.

Martin Fowler explique leurs différences en ashort glossary et les appelle collectivementtest doubles.

La simulation en Python peut être double. Tout d'abord, vous pouvez emprunter le chemin traditionnel des langages de type statique en utilisant l'injection de dépendance. Cela peut parfois vous obliger à modifier le code en cours de test, ce qui n'est pas toujours possible si le code est défini dans une bibliothèque externe:

def download(url, log=print):
    log(f'Downloading {url}')
    # ...

C'est le même exemple que j'ai utilisé dans une section précédente pour parler de la composition des fonctions. Il permet essentiellement de remplacerprint() par une fonction personnalisée de la même interface. Pour vérifier s'il imprime le bon message, vous devez l'intercepter en injectant une fonction simulée:

>>>

>>> def mock_print(message):
...     mock_print.last_message = message
...
>>> download('resource', mock_print)
>>> assert 'Downloading resource' == mock_print.last_message

L'appel de ce simulacre le fait enregistrer le dernier message dans un attribut, que vous pouvez inspecter plus tard, par exemple dans une instructionassert.

Dans une solution légèrement alternative, au lieu de remplacer toute la fonctionprint() par un wrapper personnalisé, vous pouvez rediriger la sortie standard vers un flux de caractères semblable à un fichier en mémoire:

>>>

>>> def download(url, stream=None):
...     print(f'Downloading {url}', file=stream)
...     # ...
...
>>> import io
>>> memory_buffer = io.StringIO()
>>> download('app.js', memory_buffer)
>>> download('style.css', memory_buffer)
>>> memory_buffer.getvalue()
'Downloading app.js\nDownloading style.css\n'

Cette fois, la fonction appelle explicitementprint(), mais elle expose son paramètrefile au monde extérieur.

Cependant, une manière plus pythonique de se moquer des objets tire parti du module intégrémock, qui utilise une technique appeléemonkey patching. Ce nom désobligeant vient du fait qu'il s'agit d'un «hack sale» avec lequel vous pouvez facilement vous tirer une balle dans le pied. C'est moins élégant que l'injection de dépendances mais certainement rapide et pratique.

Note: Le modulemock a été absorbé par la bibliothèque standard de Python 3, mais avant cela, il s'agissait d'un paquet tiers. Vous deviez l'installer séparément:

$ pip2 install mock

À part cela, vous l'avez appelémock, alors qu'en Python 3, il fait partie du module de test unitaire, vous devez donc importer à partir deunittest.mock.

Ce que fait le patch de singe, c'est de modifier dynamiquement l'implémentation lors de l'exécution. Un tel changement est visible à l'échelle mondiale, il peut donc avoir des conséquences indésirables. En pratique, cependant, l'application de correctifs n'affecte le code que pendant la durée de l'exécution du test.

Pour simulerprint() dans un cas de test, vous utiliserez généralement les@patchdecorator et spécifierez une cible pour le correctif en y faisant référence avec un nom complet, qui comprend le nom du module :

from unittest.mock import patch

@patch('builtins.print')
def test_print(mock_print):
    print('not a real print')
    mock_print.assert_called_with('not a real print')

Cela créera automatiquement la maquette pour vous et l'injectera dans la fonction de test. Cependant, vous devez déclarer que votre fonction de test accepte maintenant une maquette. L'objet simulé sous-jacent possède de nombreuses méthodes et attributs utiles pour vérifier le comportement.

Avez-vous remarqué quelque chose de particulier à propos de cet extrait de code?

Malgré l'injection d'une maquette à la fonction, vous ne l'appelez pas directement, bien que vous le puissiez. Cette maquette injectée n'est utilisée que pour faire des assertions par la suite et peut-être pour préparer le contexte avant d'exécuter le test.

Dans la vraie vie, la simulation permet d'isoler le code testé en supprimant les dépendances telles qu'une connexion à la base de données. Vous appelez rarement des simulacres dans un test, car cela n'a pas beaucoup de sens. Ce sont plutôt d'autres morceaux de code qui appellent indirectement votre maquette sans le savoir.

Voici ce que cela signifie:

from unittest.mock import patch

def greet(name):
    print(f'Hello, {name}!')

@patch('builtins.print')
def test_greet(mock_print):
    greet('John')
    mock_print.assert_called_with('Hello, John!')

Le code testé est une fonction qui imprime un message d'accueil. Même s'il s'agit d'une fonction assez simple, vous ne pouvez pas la tester facilement car elle ne renvoie pas de valeur. Cela a un effet secondaire.

Pour éliminer cet effet secondaire, vous devez vous moquer de la dépendance. L'application de patches vous permet d'éviter de modifier la fonction d'origine, qui peut rester indépendante deprint(). Il pense qu'il appelleprint(), mais en réalité, il appelle un simulacre dont vous avez le contrôle total.

Il existe de nombreuses raisons pour tester un logiciel. L'un d'eux recherche des bugs. Lorsque vous écrivez des tests, vous voulez souvent vous débarrasser de la fonctionprint(), par exemple, en la moquant. Paradoxalement, cependant, cette même fonction peut vous aider à trouver des bogues lors d'un processus de débogage connexe que vous lirez dans la section suivante.

Débogage d'impression

Dans cette section, vous allez jeter un œil aux outils disponibles pour le débogage en Python, à partir d'une humble fonctionprint(), en passant par le modulelogging, jusqu'à un débogueur à part entière. Après l'avoir lu, vous serez en mesure de prendre une décision éclairée sur lequel d'entre eux est le plus approprié dans une situation donnée.

Le débogage deNote: est le processus de recherche des causes profondes debugs ou des défauts du logiciel après leur découverte, ainsi que la prise de mesures pour les corriger.

Le termebug a unamusing story sur l'origine de son nom.

Tracé

Également appeléprint debugging oucaveman debugging, il s'agit de la forme de débogage la plus élémentaire. Bien qu'un peu démodé, il est toujours puissant et a ses utilisations.

L'idée est de suivre le chemin de l'exécution du programme jusqu'à ce qu'il s'arrête brusquement, ou donne des résultats incorrects, pour identifier l'instruction exacte avec un problème. Pour ce faire, vous insérez des déclarations imprimées avec des mots qui se détachent dans des endroits soigneusement choisis.

Jetez un œil à cet exemple, qui manifeste une erreur d'arrondi:

>>>

>>> def average(numbers):
...     print('debug1:', numbers)
...     if len(numbers) > 0:
...         print('debug2:', sum(numbers))
...         return sum(numbers) / len(numbers)
...
>>> 0.1 == average(3*[0.1])
debug1: [0.1, 0.1, 0.1]
debug2: 0.30000000000000004
False

Comme vous pouvez le voir, la fonction ne renvoie pas la valeur attendue de0.1, mais vous savez maintenant que c'est parce que la somme est un peu décalée. Le suivi de l'état des variables à différentes étapes de l'algorithme peut vous donner une indication de l'emplacement du problème.

Cette méthode est simple et intuitive et fonctionnera dans à peu près tous les langages de programmation. Sans oublier, c'est un excellent exercice dans le processus d'apprentissage.

D'un autre côté, une fois que vous maîtrisez des techniques plus avancées, il est difficile de revenir en arrière, car elles vous permettent de trouver les bogues beaucoup plus rapidement. Le traçage est un processus manuel laborieux, qui peut laisser passer encore plus d'erreurs. Le cycle de génération et de déploiement prend du temps. Ensuite, vous devez vous rappeler de supprimer méticuleusement tous les appelsprint() que vous avez passés sans toucher accidentellement les appels authentiques.

En outre, cela vous oblige à apporter des modifications au code, ce qui n'est pas toujours possible. Vous êtes peut-être en train de déboguer une application exécutée sur un serveur Web distant ou souhaitez diagnostiquer un problème de manièrepost-mortem. Parfois, vous n'avez tout simplement pas accès à la sortie standard.

C’est précisément là quelogging brille.

Enregistrement

Imaginons un instant que vous gérez un site Web de commerce électronique. Un jour, un client en colère passe un appel téléphonique pour se plaindre d'une transaction échouée et pour dire qu'il a perdu son argent. Il prétend avoir essayé d'acheter quelques articles, mais à la fin, une erreur cryptique l'a empêché de terminer cette commande. Pourtant, quand il a vérifié son compte bancaire, l'argent avait disparu.

Vous vous excusez sincèrement et effectuez un remboursement, mais vous ne voulez pas non plus que cela se reproduise à l'avenir. Comment déboguez-vous cela? Si seulement vous aviez une trace de ce qui s'est passé, idéalement sous la forme d'une liste chronologique des événements avec leur contexte.

Chaque fois que vous vous trouvez en train de faire du débogage d'impression, envisagez de le transformer en messages de journal permanents. Cela peut être utile dans des situations comme celle-ci, lorsque vous devez analyser un problème après qu'il s'est produit, dans un environnement auquel vous n'avez pas accès.

Il existe des outils sophistiqués pour l'agrégation et la recherche de journaux, mais au niveau le plus élémentaire, vous pouvez considérer les journaux comme des fichiers texte. Chaque ligne transmet des informations détaillées sur un événement dans votre système. Habituellement, il ne contient pas d'informations d'identification personnelle, bien que, dans certains cas, il puisse être mandaté par la loi.

Voici une ventilation d'un enregistrement de journal typique:

[2019-06-14 15:18:34,517][DEBUG][root][MainThread] Customer(id=123) logged out

Comme vous pouvez le voir, il a une forme structurée. Outre un message descriptif, il existe quelques champs personnalisables, qui fournissent le contexte d'un événement. Ici, vous avez la date et l'heure exactes, le niveau de journal, le nom de l'enregistreur et le nom du thread.

Les niveaux de journalisation vous permettent de filtrer rapidement les messages pour réduire le bruit. Si vous recherchez une erreur, vous ne voulez pas voir tous les avertissements ou messages de débogage, par exemple. Il est trivial de désactiver ou d'activer des messages à certains niveaux de journal via la configuration, sans même toucher au code.

Avec la journalisation, vous pouvez garder vos messages de débogage séparés de la sortie standard. Tous les messages de journal sont envoyés par défaut au flux d'erreurs standard, qui peut facilement apparaître dans différentes couleurs. Cependant, vous pouvez rediriger les messages du journal vers des fichiers séparés, même pour des modules individuels!

Très souvent, une journalisation mal configurée peut entraîner un manque d’espace sur le disque du serveur. Pour éviter cela, vous pouvez configurerlog rotation, qui conservera les fichiers journaux pendant une durée spécifiée, telle qu'une semaine, ou une fois qu'ils auront atteint une certaine taille. Néanmoins, il est toujours recommandé d'archiver les journaux plus anciens. Certaines réglementations imposent que les données des clients soient conservées pendant cinq ans!

Comparé à d'autres langages de programmation,logging in Python est plus simple, car le modulelogging est fourni avec la bibliothèque standard. Vous venez de l'importer et de le configurer en seulement deux lignes de code:

import logging
logging.basicConfig(level=logging.DEBUG)

Vous pouvez appeler des fonctions définies au niveau du module, qui sont accrochées auxroot logger, mais plus la pratique courante est d'obtenir un enregistreur dédié pour chacun de vos fichiers source:

logging.debug('hello')  # Module-level function

logger = logging.getLogger(__name__)
logger.debug('hello')   # Logger's method

L'avantage d'utiliser des enregistreurs personnalisés est un contrôle plus fin. Ils sont généralement nommés d'après le module dans lequel ils ont été définis via la variable__name__.

Note: Il existe un modulewarnings quelque peu lié en Python, qui peut également enregistrer les messages dans le flux d'erreur standard. Cependant, son spectre d'applications est plus restreint, principalement en code de bibliothèque, alors que les applications clientes devraient utiliser le modulelogging.

Cela dit, vous pouvez les faire fonctionner ensemble en appelantlogging.captureWarnings(True).

Une dernière raison pour passer de la fonctionprint() à la journalisation est la sécurité des threads. Dans la section à venir, vous verrez que le premier ne fonctionne pas bien avec plusieurs threads d'exécution.

Débogage

La vérité est que ni le traçage ni la journalisation ne peuvent être considérés comme un véritable débogage. Pour effectuer le débogage réel, vous avez besoin d'un outil de débogage, qui vous permet d'effectuer les opérations suivantes:

  • Parcourez le code de manière interactive.

  • Définissez des points d'arrêt, y compris des points d'arrêt conditionnels.

  • Variables introspectives en mémoire.

  • Évaluez les expressions personnalisées au moment de l'exécution.

Un débogueur grossier qui s'exécute dans le terminal, nommé sans surprisepdb pour «Le débogueur Python», est distribué dans le cadre de la bibliothèque standard. Cela le rend toujours disponible, il peut donc être votre seul choix pour effectuer le débogage à distance. C'est peut-être une bonne raison de s'y familiariser.

Cependant, il ne vient pas avec une interface graphique, doncusing pdb peut être un peu délicat. Si vous ne pouvez pas modifier le code, vous devez l'exécuter en tant que module et transmettre l'emplacement de votre script:

$ python -m pdb my_script.py

Sinon, vous pouvez configurer un point d'arrêt directement dans le code, ce qui suspendra l'exécution de votre script et vous déposera dans le débogueur. L'ancienne façon de procéder exigeait deux étapes:

>>>

>>> import pdb
>>> pdb.set_trace()
--Return--
> (1)()->None
(Pdb)

Cela affiche une invite interactive, qui peut sembler intimidante au premier abord. Cependant, vous pouvez toujours taper Python natif à ce stade pour examiner ou modifier l'état des variables locales. En dehors de cela, il n'y a vraiment qu'une poignée de commandes spécifiques au débogueur que vous souhaitez utiliser pour parcourir le code.

Note: Il est habituel de placer les deux instructions pour lancer un débogueur sur une seule ligne. Cela nécessite l'utilisation d'un point-virgule, que l'on trouve rarement dans les programmes Python:

import pdb; pdb.set_trace()

Bien qu'il ne s'agisse certainement pas de Pythonic, il s'agit d'un rappel pour le supprimer une fois le débogage terminé.

Depuis Python 3.7, vous pouvez également appeler la fonction intégréebreakpoint(), qui fait la même chose, mais de manière plus compacte et avec quelquesbells and whistles supplémentaires:

def average(numbers):
    if len(numbers) > 0:
        breakpoint()  # Python 3.7+
        return sum(numbers) / len(numbers)

Vous allez probablement utiliser un débogueur visuel intégré à un éditeur de code pour la plupart. PyCharm possède un excellent débogueur, qui offre des performances élevées, mais vous trouverezplenty of alternative IDEs avec des débogueurs, à la fois payants et gratuits.

Le débogage n'est pas la solution miracle proverbiale. Parfois, la journalisation ou le traçage sera une meilleure solution. Par exemple, les défauts difficiles à reproduire, tels querace conditions, résultent souvent d'un couplage temporel. Lorsque vous vous arrêtez à un point d'arrêt, cette petite pause dans l'exécution du programme peut masquer le problème. C'est un peu comme lesHeisenberg principle: vous ne pouvez pas mesurer et observer un bogue en même temps.

Ces méthodes ne s'excluent pas mutuellement. Ils se complètent.

Impression sans fil

J'ai brièvement abordé le problème de la sécurité des threads avant, recommandantlogging sur la fonctionprint(). Si vous lisez toujours ceci, alors vous devez être à l'aise avecthe concept of threads.

La sécurité des threads signifie qu'un morceau de code peut être partagé en toute sécurité entre plusieurs threads d'exécution. La stratégie la plus simple pour garantir la sécurité des threads consiste à ne partager que les objetsimmutable. Si les threads ne peuvent pas modifier l'état d'un objet, il n'y a aucun risque de rompre sa cohérence.

Une autre méthode tire parti delocal memory, qui permet à chaque thread de recevoir sa propre copie du même objet. De cette façon, les autres threads ne peuvent pas voir les modifications qui y sont apportées dans le thread actuel.

Mais cela ne résout pas le problème, n'est-ce pas? Vous souhaitez souvent que vos threads coopèrent en pouvant muter une ressource partagée. Le moyen le plus courant de synchroniser l'accès simultané à une telle ressource est delocking it. Cela donne un accès en écriture exclusif à un ou parfois à quelques threads à la fois.

Cependant, le verrouillage est coûteux et réduit le débit simultané, de sorte que d'autres moyens de contrôle d'accès ont été inventés, tels queatomic variables ou l'algorithmecompare-and-swap.

L'impression n'est pas thread-safe en Python. La fonctionprint() contient une référence à la sortie standard, qui est une variable globale partagée. En théorie, comme il n'y a pas de verrouillage, un changement de contexte peut se produire lors d'un appel àsys.stdout.write(), entrelaçant des bits de texte provenant de plusieurs appels àprint().

Note: Un changement de contexte signifie qu'un thread arrête son exécution, volontairement ou non, afin qu'un autre puisse prendre le relais. Cela peut arriver à tout moment, même au milieu d'un appel de fonction.

En pratique, cependant, cela ne se produit pas. Peu importe vos efforts, l'écriture sur la sortie standard semble atomique. Le seul problème que vous pouvez parfois observer est celui des sauts de ligne foirés:

[Thread-3 A][Thread-2 A][Thread-1 A]

[Thread-3 B][Thread-1 B]


[Thread-1 C][Thread-3 C]

[Thread-2 B]
[Thread-2 C]

Pour simuler cela, vous pouvez augmenter la probabilité d'un changement de contexte en mettant la méthode sous-jacente.write() en veille pendant une durée aléatoire. How? En le moquant, que vous connaissez déjà dans une section précédente:

import sys

from time import sleep
from random import random
from threading import current_thread, Thread
from unittest.mock import patch

write = sys.stdout.write

def slow_write(text):
    sleep(random())
    write(text)

def task():
    thread_name = current_thread().name
    for letter in 'ABC':
        print(f'[{thread_name} {letter}]')

with patch('sys.stdout') as mock_stdout:
    mock_stdout.write = slow_write
    for _ in range(3):
        Thread(target=task).start()

Tout d'abord, vous devez stocker la méthode.write() d'origine dans une variable, à laquelle vous déléguerez plus tard. Ensuite, vous fournissez votre fausse implémentation, qui prendra jusqu'à une seconde pour s'exécuter. Chaque thread effectuera quelques appelsprint() avec son nom et une lettre: A, B et C.

Si vous avez lu la section moqueuse avant, vous avez peut-être déjà une idée des raisons pour lesquelles l'impression se comporte mal comme ça. Néanmoins, pour le rendre clair, vous pouvez capturer des valeurs introduites dans votre fonctionslow_write(). Vous remarquerez que vous obtenez une séquence légèrement différente à chaque fois:

[
    '[Thread-3 A]',
    '[Thread-2 A]',
    '[Thread-1 A]',
    '\n',
    '\n',
    '[Thread-3 B]',
    (...)
]

Même sisys.stdout.write() lui-même est une opération atomique, un seul appel à la fonctionprint() peut donner plus d'une écriture. Par exemple, les sauts de ligne sont écrits séparément du reste du texte et le changement de contexte a lieu entre ces écritures.

Note: La nature atomique de la sortie standard en Python est un sous-produit desGlobal Interpreter Lock, qui applique un verrouillage autour des instructions bytecode. Sachez toutefois que de nombreuses versions d'interpréteur n'ont pas le GIL, où l'impression multi-thread nécessite un verrouillage explicite.

Vous pouvez faire en sorte que le caractère de nouvelle ligne fasse partie intégrante du message en le traitant manuellement:

print(f'[{thread_name} {letter}]\n', end='')

Cela corrigera la sortie:

[Thread-2 A]
[Thread-1 A]
[Thread-3 A]
[Thread-1 B]
[Thread-3 B]
[Thread-2 B]
[Thread-1 C]
[Thread-2 C]
[Thread-3 C]

Notez, cependant, que la fonctionprint() continue de faire un appel séparé pour le suffixe vide, ce qui se traduit par une instructionsys.stdout.write('') inutile:

[
    '[Thread-2 A]\n',
    '[Thread-1 A]\n',
    '[Thread-3 A]\n',
    '',
    '',
    '',
    '[Thread-1 B]\n',
    (...)
]

Une version véritablement thread-safe de la fonctionprint() pourrait ressembler à ceci:

import threading

lock = threading.Lock()

def thread_safe_print(*args, **kwargs):
    with lock:
        print(*args, **kwargs)

Vous pouvez mettre cette fonction dans un module et l'importer ailleurs:

from thread_safe_print import thread_safe_print

def task():
    thread_name = current_thread().name
    for letter in 'ABC':
        thread_safe_print(f'[{thread_name} {letter}]')

Maintenant, malgré deux écritures pour chaque requêteprint(), un seul thread est autorisé à interagir avec le flux, tandis que le reste doit attendre:

[
    # Lock acquired by Thread-3
    '[Thread-3 A]',
    '\n',
    # Lock released by Thread-3
    # Lock acquired by Thread-1
    '[Thread-1 B]',
    '\n',
    # Lock released by Thread-1
    (...)
]

J'ai ajouté des commentaires pour indiquer comment le verrou limite l'accès à la ressource partagée.

Note: Même dans le code monothread, vous pourriez être pris dans une situation similaire. Plus précisément, lorsque vous imprimez sur la sortie standard et les flux d’erreurs standard en même temps. À moins que vous ne redirigiez l'un d'entre eux ou les deux vers des fichiers distincts, ils partageront tous les deux une seule fenêtre de terminal.

À l'inverse, le modulelogging est de par sa conception thread-safe, ce qui se traduit par sa capacité à afficher les noms de thread dans le message formaté:

>>>

>>> import logging
>>> logging.basicConfig(format='%(threadName)s %(message)s')
>>> logging.error('hello')
MainThread hello

C’est une autre raison pour laquelle vous ne voudrez peut-être pas utiliser la fonctionprint() tout le temps.

Contreparties d'impression Python

A présent, vous savez tout ce qu'il y a à savoir surprint()! Le sujet, cependant, ne serait pas complet sans parler un peu de ses homologues. Alors queprint() concerne la sortie, il existe des fonctions et des bibliothèques pour l'entrée.

Intégré

Python est livré avec une fonction intégrée pour accepter les entrées de l'utilisateur, appelée de manière prévisibleinput(). Il accepte les données du flux d'entrée standard, qui est généralement le clavier:

>>>

>>> name = input('Enter your name: ')
Enter your name: jdoe
>>> print(name)
jdoe

La fonction renvoie toujours une chaîne, vous devrez donc peut-être l'analyser en conséquence:

try:
    age = int(input('How old are you? '))
except ValueError:
    pass

Le paramètre prompt est complètement facultatif, donc rien ne s'affichera si vous le sautez, mais la fonction fonctionnera toujours:

>>>

>>> x = input()
hello world
>>> print(x)
hello world

Néanmoins, lancer un appel à l'action descriptif rend l'expérience utilisateur bien meilleure.

Note: Pour lire à partir de l'entrée standard de Python 2, vous devez appelerraw_input() à la place, ce qui est encore un autre élément intégré. Malheureusement, il existe également une fonctioninput() mal nommée, qui fait une chose légèrement différente.

En fait, il prend également l'entrée du flux standard, mais il essaie ensuite de l'évaluer comme s'il s'agissait de code Python. Comme c'est un potentielsecurity vulnerability, cette fonction a été complètement supprimée de Python 3, tandis queraw_input() a été renommé eninput().

Voici une comparaison rapide des fonctions disponibles et de leurs fonctions:

Python 2 Python 3

raw_input()

input()

input()

eval(input())

Comme vous pouvez le constater, il est toujours possible de simuler l'ancien comportement dans Python 3.

Demander à l'utilisateur un mot de passe avecinput() est une mauvaise idée car il apparaîtra en texte brut au fur et à mesure qu'il le saisit. Dans ce cas, vous devriez utiliser la fonctiongetpass() à la place, qui masque les caractères tapés. Cette fonction est définie dans un module du même nom, également disponible dans la bibliothèque standard:

>>>

>>> from getpass import getpass
>>> password = getpass()
Password:
>>> print(password)
s3cret

Le modulegetpass a une autre fonction pour obtenir le nom de l’utilisateur à partir d’une variable d’environnement:

>>>

>>> from getpass import getuser
>>> getuser()
'jdoe'

Les fonctions intégrées de Python pour gérer l'entrée standard sont assez limitées. Dans le même temps, il existe de nombreux packages tiers, qui offrent des outils beaucoup plus sophistiqués.

Tierce personne

Il existe des packages Python externes qui permettent de créer des interfaces graphiques complexes spécifiquement pour collecter des données auprès de l'utilisateur. Certaines de leurs fonctionnalités incluent:

  • Formatage et style avancés

  • Analyse, validation et nettoyage automatisés des données utilisateur

  • Un style déclaratif de définition des dispositions

  • Saisie semi-automatique interactive

  • Prise en charge de la souris

  • Widgets prédéfinis tels que listes de contrôle ou menus

  • Historique de recherche des commandes tapées

  • Mise en évidence de la syntaxe

La démonstration de tels outils n'entre pas dans le cadre de cet article, mais vous pouvez les essayer. J'ai personnellement appris à connaître certains d'entre eux grâce auxPython Bytes Podcast. Les voici:

Néanmoins, il convient de mentionner un outil de ligne de commande appelérlwrap qui ajoute gratuitement de puissantes fonctionnalités d'édition de ligne à vos scripts Python. Vous n'avez rien à faire pour que cela fonctionne!

Supposons que vous ayez écrit une interface de ligne de commande qui comprend trois instructions, dont une pour ajouter des nombres:

print('Type "help", "exit", "add a [b [c ...]]"')
while True:
    command, *arguments = input('~ ').split(' ')
    if len(command) > 0:
        if command.lower() == 'exit':
            break
        elif command.lower() == 'help':
            print('This is help.')
        elif command.lower() == 'add':
            print(sum(map(int, arguments)))
        else:
            print('Unknown command')

À première vue, cela semble être une invite typique lorsque vous l'exécutez:

$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ add 1 2 3 4
10
~ aad 2 3
Unknown command
~ exit
$

Mais dès que vous faites une erreur et que vous souhaitez la corriger, vous verrez qu'aucune des touches de fonction ne fonctionne comme prévu. Frapper la flèche[.kbd .key-arrow-left]#Left #, par exemple, entraîne ceci au lieu de déplacer le curseur vers l'arrière:

$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ aad^[[D

Maintenant, vous pouvez encapsuler le même script avec la commanderlwrap. Non seulement vous obtiendrez les touches fléchées fonctionnelles, mais vous pourrez également rechercher dans l'historique persistant de vos commandes personnalisées, utiliser la saisie semi-automatique et modifier la ligne avec des raccourcis:

$ rlwrap python calculator.py
Type "help", "exit", "add a [b [c ...]]"
(reverse-i-search)`a': add 1 2 3 4

N'est-ce pas génial?

Conclusion

Vous disposez désormais d'un ensemble de connaissances sur la fonctionprint() en Python, ainsi que sur de nombreux sujets environnants. Vous avez une compréhension approfondie de ce que c'est et comment cela fonctionne, impliquant tous ses éléments clés. De nombreux exemples vous ont donné un aperçu de son évolution à partir de Python 2.

En dehors de cela, vous avez appris à:

  • Évitez les erreurs courantes avecprint() en Python

  • Traitez les nouvelles lignes, les encodages de caractères et la mise en mémoire tampon

  • Écrire du texte dans des fichiers

  • Simulez la fonctionprint() dans les tests unitaires

  • Créer des interfaces utilisateur avancées dans le terminal

Maintenant que vous savez tout cela, vous pouvez créer des programmes interactifs qui communiquent avec les utilisateurs ou produisent des données dans des formats de fichiers courants. Vous pouvez diagnostiquer rapidement les problèmes dans votre code et vous en protéger. Enfin, vous savez comment mettre en œuvre le jeu de serpent classique.

Si vous avez toujours soif de plus d'informations, avez des questions ou souhaitez simplement partager vos réflexions, n'hésitez pas à nous contacter dans la section commentaires ci-dessous.