Une fonction retournant un générateur (c'est-à-dire une fonction avec une fonction yield
) dans l'une de nos bibliothèques échoue à certains tests en raison d'une erreur de manipulation de l'instruction StopIteration
exception. Par commodité, dans ce billet, je ferai référence à cette fonction en tant que buggy
.
Je n'ai pas été en mesure de trouver un moyen pour buggy
pour empêcher l'exception (sans affecter le fonctionnement normal de la fonction). De même, je n'ai pas trouvé de moyen de piéger l'exception (avec une fonction de type try
/ except
) dans buggy
.
( Code client en utilisant buggy
peut piéger cette exception, mais cela arrive trop tard, car le code qui dispose des informations nécessaires pour traiter correctement la condition conduisant à cette exception est le code buggy
fonction.)
Le code et le scénario de test sur lesquels je travaille sont bien trop compliqués pour être publiés ici. J'ai donc créé un modèle très simple, mais aussi très simple, de test d'application. extrêmement artificielle exemple de jouet qui illustre le problème.
Tout d'abord, le module avec le buggy
fonction :
# mymod.py
import csv # essential!
def buggy(csvfile):
with open(csvfile) as stream:
reader = csv.reader(stream)
# how to test *here* if either stream is at its end?
for row in reader:
yield row
Comme indiqué par le commentaire, l'utilisation du csv
(de la bibliothèque standard de Python 3.x) est une caractéristique essentielle de ce problème. 1 .
Le fichier suivant de l'exemple est un script qui a pour but de remplace "code client". . En d'autres termes, le "but réel" de ce script au-delà de cet exemple est largement hors de propos. Son rôle dans l'exemple est de fournir un moyen simple et fiable d'éliciter le problème avec la fonction buggy
fonction. (Une partie de son code pourrait être réutilisée pour un scénario de test dans une suite de tests, par exemple).
#!/usr/bin/env python3
# myscript.py
import sys
import mymod
def print_row(row):
print(*row, sep='\t')
def main(csvfile, mode=None):
if mode == 'first':
print_row(next(mymod.buggy(csvfile)))
else:
for row in mymod.buggy(csvfile):
print_row(row)
if __name__ == '__main__':
main(*sys.argv[1:])
Le script prend le chemin d'un fichier CSV comme argument obligatoire, et un second argument optionnel. Si le second argument est omis, ou s'il s'agit d'autre chose que la chaîne de caractères "first"
le script s'imprimera à stdout
les informations contenues dans le fichier CSV, mais en TSV format. Si le deuxième argument est la chaîne de caractères "first"
seules les informations de la première ligne seront imprimées.
El StopIteration
L'exception que j'essaie de piéger survient lorsque myscript.py
script est invoqué avec un fichier vide et la chaîne de caractères "first"
comme arguments 2 .
Voici un exemple de ce code en action :
% cat ok_input.csv
1,2,3
4,5,6
7,8,9
% ./myscript.py ok_input.csv
1 2 3
4 5 6
7 8 9
% ./myscript.py ok_input.csv first
1 2 3
% cat empty_input.csv
# no output (of course)
% ./myscript.py empty_input.csv
# no output (as desired)
% ./myscript.py empty_input.csv first
Traceback (most recent call last):
File "./myscript.py", line 19, in <module>
main(*sys.argv[1:])
File "./myscript.py", line 13, in main
print_row(next(mymod.buggy(csvfile)))
StopIteration
Q : Comment puis-je prévenir ou piéger cette StopIteration
dans le champ lexical de l'élément buggy
fonction ?
IMPORTANT : Gardez à l'esprit que, dans l'exemple donné ci-dessus, l'option myscript.py
script est un stand-in pour "code client", et est donc hors de notre contrôle. Cela signifie que toute approche qui nécessiterait de modifier l'élément myscript.py
script ne résoudrait pas le problème réel du monde, et ne serait donc pas une réponse acceptable à cette question.
Une différence importante entre l'exemple simple présenté ci-dessus et notre situation réelle est que dans notre cas, le flux d'entrée problématique ne provient pas d'un fichier vide. Le problème se pose dans les cas où buggy
(ou, plutôt, sa contrepartie dans le monde réel) atteint la fin de ce flux "trop tôt", pour ainsi dire.
Je pense qu'il suffirait de tester si soit stream
est à sa fin, avant que le for row in reader:
mais je n'ai pas non plus trouvé le moyen de le faire. Tester si la valeur renvoyée par stream.read(1)
est 0 ou 1 me dira si le flux est à sa fin, mais dans le dernier cas stream
Le pointeur interne de l'utilisateur se retrouvera à pointer un octet de trop dans le dossier de l'utilisateur. csvfile
Le contenu du site. (Ni l'un ni l'autre stream.seek(-1, 1)
ni stream.tell()
travail à ce stade).
Enfin, pour tous ceux qui souhaitent répondre à cette question, il serait plus efficace de profiter de l'exemple de code que j'ai fourni ci-dessus pour tester votre proposition avant de la poster.
EDIT : Une variante de mymod.py
que j'ai essayé était la suivante :
import csv # essential!
def buggy(csvfile):
with open(csvfile) as stream:
reader = csv.reader(stream)
try:
firstrow = next(reader)
except StopIteration:
firstrow = None
if firstrow != None:
yield firstrow
for row in reader:
yield row
Cette variante échoue avec à peu près le même message d'erreur que la version originale.
Lorsque j'ai lu pour la première fois la proposition de @mcernak, j'ai pensé qu'elle était assez similaire à la variation ci-dessus, et je m'attendais donc à ce qu'elle échoue également. Puis j'ai été agréablement surpris de découvrir que ce n'est pas le cas ! Par conséquent, à partir de maintenant, il y a un candidat défini pour obtenir la prime. Cela dit, J'aimerais comprendre pourquoi la variation ci-dessus ne parvient pas à piéger l'exception, alors que celle de @mcernak y parvient.
1 Dans le cas présent, il s'agit d'un code hérité ; le passage de l'option csv
Le module vers une alternative n'est pas une option pour nous à court terme.
2 S'il vous plaît, ignorez entièrement la question de savoir quelle devrait être la "bonne réponse" de ce script de démonstration lorsqu'il est invoqué avec un fichier vide et la chaîne de caractères "first"
comme arguments. La combinaison particulière d'entrées qui suscite la StopIteration
dans la démonstration de ce billet ne représente pas la condition réelle qui fait que notre code émet l'exception problématique StopIteration
exception. Par conséquent, la "réponse correcte", quelle qu'elle soit, de la démonstration script au fichier vide plus "first"
La combinaison de chaînes de caractères ne serait pas pertinente pour le problème réel auquel je fais face.