Comment attraper un avertissement numpy comme si c'était une exception (pas seulement pour les tests)?

174

Je dois créer un polynôme de Lagrange en Python pour un projet que je fais. Je fais un style barycentrique pour éviter d'utiliser une boucle for explicite par opposition à un style de différence divisée de Newton. Le problème que j'ai est que je dois attraper une division par zéro, mais Python (ou peut-être numpy) en fait juste un avertissement au lieu d'une exception normale.

Donc, ce que j'ai besoin de savoir comment faire est d'attraper cet avertissement comme s'il s'agissait d'une exception. Les questions connexes que j'ai trouvées sur ce site n'ont pas reçu de réponse de la manière dont j'avais besoin. Voici mon code:

import numpy as np
import matplotlib.pyplot as plt
import warnings

class Lagrange:
    def __init__(self, xPts, yPts):
        self.xPts = np.array(xPts)
        self.yPts = np.array(yPts)
        self.degree = len(xPts)-1 
        self.weights = np.array([np.product([x_j - x_i for x_j in xPts if x_j != x_i]) for x_i in xPts])

    def __call__(self, x):
        warnings.filterwarnings("error")
        try:
            bigNumerator = np.product(x - self.xPts)
            numerators = np.array([bigNumerator/(x - x_j) for x_j in self.xPts])
            return sum(numerators/self.weights*self.yPts) 
        except Exception, e: # Catch division by 0. Only possible in 'numerators' array
            return yPts[np.where(xPts == x)[0][0]]

L = Lagrange([-1,0,1],[1,0,1]) # Creates quadratic poly L(x) = x^2

L(1) # This should catch an error, then return 1. 

Lorsque ce code est exécuté, la sortie que j'obtiens est:

Warning: divide by zero encountered in int_scalars

C'est l'avertissement que je veux attraper. Cela devrait se produire dans la compréhension de la liste.

John K.
la source
2
En êtes-vous sûr Warning: ...? Essayer des choses comme np.array([1])/0moi RuntimeWarning: ...en sortie.
Bakuriu
1
@MadPhysicist Pas un doublon; NumPy a sa propre architecture d'avertissement interne au-dessus des pythons, qui peut être spécifiquement contrôlée (voir la réponse de Bakuríu).
gerrit
@gerrit. Je suis corrigé et j'ai appris une nouvelle chose. J'ai supprimé mon commentaire d'origine pour éviter de déclencher une frénésie de collecte de badges.
Mad Physicist
Une autre approche que vous pourriez utiliser est de simplement vérifier si le dénominateur est 0 avant la division, ce qui évite la surcharge de jouer avec le système d'avertissement de numpy. (Bien que cela signifie probablement que vous devez étendre la compréhension de la liste soignée dans une boucle vérifiant si l'un des dénominateurs est nul.)
Oliver

Réponses:

198

Il semble que votre configuration utilise l' printoption pour numpy.seterr:

>>> import numpy as np
>>> np.array([1])/0   #'warn' mode
__main__:1: RuntimeWarning: divide by zero encountered in divide
array([0])
>>> np.seterr(all='print')
{'over': 'warn', 'divide': 'warn', 'invalid': 'warn', 'under': 'ignore'}
>>> np.array([1])/0   #'print' mode
Warning: divide by zero encountered in divide
array([0])

Cela signifie que l'avertissement que vous voyez n'est pas un véritable avertissement, mais que ce n'est que quelques caractères imprimés stdout(voir la documentation pour seterr). Si vous voulez l'attraper, vous pouvez:

  1. Utilisez numpy.seterr(all='raise')qui lèvera directement l'exception. Cela change cependant le comportement de toutes les opérations, donc c'est un assez gros changement de comportement.
  2. Utilisez numpy.seterr(all='warn'), qui transformera l'avertissement imprimé en un véritable avertissement et vous pourrez utiliser la solution ci-dessus pour localiser ce changement de comportement.

Une fois que vous avez réellement un avertissement, vous pouvez utiliser le warningsmodule pour contrôler la façon dont les avertissements doivent être traités:

>>> import warnings
>>> 
>>> warnings.filterwarnings('error')
>>> 
>>> try:
...     warnings.warn(Warning())
... except Warning:
...     print 'Warning was raised as an exception!'
... 
Warning was raised as an exception!

Lisez attentivement la documentation filterwarningscar elle vous permet de filtrer uniquement l'avertissement que vous souhaitez et dispose d'autres options. J'envisagerais également de regarder catch_warningsquel est un gestionnaire de contexte qui réinitialise automatiquement la filterwarningsfonction d' origine :

>>> import warnings
>>> with warnings.catch_warnings():
...     warnings.filterwarnings('error')
...     try:
...         warnings.warn(Warning())
...     except Warning: print 'Raised!'
... 
Raised!
>>> try:
...     warnings.warn(Warning())
... except Warning: print 'Not raised!'
... 
__main__:2: Warning: 
Bakuriu
la source
Je pense que c'est un début. Mais cela ne résout pas vraiment mon problème. Si j'ajoute warnings.warn (Warning ())) dans mon code dans le bloc try, il interceptera l'avertissement. Pour une raison quelconque, il n'attrape pas l'avertissement de division par zéro. Voici le message d'avertissement exact: Attention: division par zéro rencontrée dans int_scalars
John K.
@JohnK. Vous devez modifier votre question et ajouter la sortie exacte, sinon nous ne pouvons pas dire ce qui ne va pas. Il pourrait être possible que numpy définit cette classe avertissement quelque part et vous devez Discovere dans lequel subpackage de pouvoir l' attraper. Qu'à cela ne tienne, j'ai découvert que vous devriez utiliser RuntimeWarning. Mise à jour de la réponse.
Bakuriu
Êtes-vous sûr? J'ai changé mon code pour utiliser sauf RuntimeWarning :. Cela ne fonctionne toujours pas = /
John K.
@JohnK. Dans la documentation, il indique que a RuntimeWarningest levé. Le problème peut être que votre configuration numpy utilise l' printoption, qui imprime simplement l'avertissement mais ce n'est pas un vrai avertissement géré par le warningsmodule ... Si tel est le cas, vous pouvez essayer d'utiliser numpy.seterr(all='warn')et réessayer.
Bakuriu
3
Dans ma version de numpy, vous ne pouvez pas utiliser numpy.seterr(all='error'), errordoit l'être raise.
detly
41

Pour ajouter un peu à la réponse de @ Bakuriu:

Si vous savez déjà où l'avertissement est susceptible de se produire, il est souvent plus propre d'utiliser le numpy.errstategestionnaire de contexte, plutôt que celui numpy.seterrqui traite tous les avertissements ultérieurs du même type de la même manière, quel que soit l'endroit où ils se produisent dans votre code:

import numpy as np

a = np.r_[1.]
with np.errstate(divide='raise'):
    try:
        a / 0   # this gets caught and handled as an exception
    except FloatingPointError:
        print('oh no!')
a / 0           # this prints a RuntimeWarning as usual

Éditer:

Dans mon exemple original, j'avais a = np.r_[0], mais apparemment il y avait un changement dans le comportement de numpy tel que la division par zéro est gérée différemment dans les cas où le numérateur est entièrement zéros. Par exemple, dans numpy 1.16.4:

all_zeros = np.array([0., 0.])
not_all_zeros = np.array([1., 0.])

with np.errstate(divide='raise'):
    not_all_zeros / 0.  # Raises FloatingPointError

with np.errstate(divide='raise'):
    all_zeros / 0.  # No exception raised

with np.errstate(invalid='raise'):
    all_zeros / 0.  # Raises FloatingPointError

Les messages d'avertissement correspondants sont également différents: 1. / 0.est enregistré comme RuntimeWarning: divide by zero encountered in true_divide, alors que 0. / 0.est enregistré comme RuntimeWarning: invalid value encountered in true_divide. Je ne sais pas pourquoi exactement ce changement a été effectué, mais je soupçonne que cela a à voir avec le fait que le résultat de 0. / 0.n'est pas représentable sous forme de nombre (numpy renvoie un NaN dans ce cas) alors que 1. / 0.et -1. / 0.return + Inf et -Inf respectivement , conformément à la norme IEE 754.

Si vous voulez intercepter les deux types d'erreur, vous pouvez toujours passer np.errstate(divide='raise', invalid='raise'), ou all='raise'si vous voulez lever une exception sur n'importe quel type d'erreur en virgule flottante.

ali_m
la source
Notamment cela soulève FloatingPointError, non ZeroDivisionError.
gerrit
Cela ne fonctionne pas Python 3.6.3avec numpy==1.16.3. Pourriez-vous le mettre à jour s'il vous plaît?
anilbey
1
@anilbey Apparemment, il y a eu un changement dans le comportement de numpy qui signifie que la division par zéro est maintenant gérée différemment selon que le numérateur est également (tout) zéro.
ali_m
27

Pour élaborer sur la réponse de @ Bakuriu ci-dessus, j'ai trouvé que cela me permettait d'attraper un avertissement d'exécution de la même manière que j'attraperais un avertissement d'erreur, en imprimant l'avertissement bien:

import warnings

with warnings.catch_warnings():
    warnings.filterwarnings('error')
    try:
        answer = 1 / 0
    except Warning as e:
        print('error found:', e)

Vous pourrez probablement jouer avec le placement du placement warnings.catch_warnings () en fonction de la taille d'un parapluie que vous voulez lancer avec des erreurs de capture de cette façon.

ntk4
la source
3
answer =
1/0 générera
8

Supprimez warnings.filterwarnings et ajoutez:

numpy.seterr(all='raise')
Shital Shah
la source