6 votes

Comment prévenir ou piéger l'exception StopIteration dans la fonction d'appel de rendement ?

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.

5voto

mcernak Points 8240

Vous pouvez piéger le StopIteration dans la portée lexicale de l'élément buggy fonctionnent de cette manière :

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        try:
            yield next(reader)
        except StopIteration:
            yield 'dummy value'

        for row in reader:
            yield row

En fait, vous demandez manuellement la première valeur de la base de données de l'UE. reader itérateur et

  • si cela réussit, la première ligne est lue depuis le fichier csv et est cédée à l'appelant de la fonction buggy fonction
  • si cela échoue, comme c'est le cas pour les fichiers csv vides, une chaîne de caractères, par ex. dummy value est cédé afin d'empêcher l'appelant de la fonction buggy d'empêcher la fonction de planter

Ensuite, si le fichier csv n'était pas vide, les lignes restantes seront lues (et cédées) dans le cycle for.


EDIT : pour illustrer pourquoi l'autre variation de mymod.py mentionné dans la question ne fonctionne pas, j'y ai ajouté quelques instructions d'impression :

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        try:
            print('reading first row')
            firstrow = next(reader)
        except StopIteration:
            print('no first row exists')
            firstrow = None

        if firstrow != None:
            print('yielding first row: ' + firstrow)
            yield firstrow

        for row in reader:
            print('yielding next row: ' + row)
            yield row

        print('exiting function open')

Son exécution donne le résultat suivant :

% ./myscript.py empty_input.csv first
reading first row
no first row exists
exiting function open
Traceback (most recent call last):
  File "myscript.py", line 15, in <module>
    main(*sys.argv[1:])
  File "myscript.py", line 9, in main
    print_row(next(mymod.buggy(csvfile)))

Cela montre que, dans le cas où le fichier d'entrée est vide, le premier fichier d'entrée est le premier fichier d'entrée. try..except gère correctement le bloc StopIteration et que le buggy La fonction se poursuit normalement.
L'exception que l'appelant de la méthode buggy obtient dans ce cas est due au fait que la buggy ne donne aucune valeur avant de se terminer.

1voto

ti7 Points 1865

mcernak résout et décrit bien le problème que vous avez

Cependant, il existe un problème de conception sous-jacent : l'appelant parfois n'attend pas un générateur, mais un itérateur non vide.

Si l'on considère la question sous un autre angle, que doit-il se passer si le fichier est manquant ? Serait-il plus logique que la fonction gère IOError de open et retourner une sentinelle ou l'élever à l'appelant ?

Au lieu d'essayer de contraindre votre générateur à travailler avec des appelants qui le maltraitent, envisagez de

  • fournir deux fonctions (l'une pouvant appeler l'autre)
  • fournir un argument pour un compte maximal de rangs du générateur (probablement le meilleur)

    mymod.py

    import csv import itertools def notbuggy(csvfile, max_rows=None): with open(csvfile) as stream: yield from itertools.islice(csv.reader(stream), max_rows)

    !/usr/bin/env python3

    myscript.py

    import sys import mymod

    def print_row(row): print(*row, sep='\t')

    def main(csvfile, mode=None): max_rows = 1 if mode == "first" else None for row in mymod.notbuggy(csvfile, max_rows): print_row(row)

    if name == 'main': main(*sys.argv[1:])


Lorsque vous utilisez next() la logique d'appel doit accepter l'un des éléments suivants

  • ne jamais l'appeler sur un itérable vide (vérifier le fichier d'abord ?)
  • gérer une exception provenant du générateur ( StopIteration certains personnalisés Exception )
  • manipuler une sentinelle vide (peut-être "" une certaine ficelle, None ou object ..)

Cependant, l'appelant ne fait rien de tout cela, donc les garanties ne sont pas bien établies !

Que se passe-t-il si l'appelant veut plus qu'une seule ligne ou interprète la sentinelle vide comme une valeur ? À moins que ces éléments ne soient communiqués d'une manière ou d'une autre dans la documentation, l'appelant peut toujours mal utiliser une fonction et ne pas savoir pourquoi elle a un comportement inattendu.

>>> next(iter(()))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> g = iter((1,))
>>> next(g)
1
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> print_row("STOP SENTINEL")
S   T   O   P       S   E   N   T   I   N   E   L

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X