38 votes

erreurs d'arrondi dans la division du sol en Python

Je sais que les erreurs d'arrondi se produisent dans l'arithmétique à virgule flottante, mais quelqu'un peut-il expliquer la raison de celle-ci ?

>>> 8.0 / 0.4  # as expected
20.0
>>> floor(8.0 / 0.4)  # int works too
20
>>> 8.0 // 0.4  # expecting 20.0
19.0

Cela se produit avec Python 2 et 3 sur x64.

D'après ce que je vois, il s'agit soit d'un bogue, soit d'une spécification très stupide de l'outil de gestion de l'environnement. // car je ne vois pas pourquoi la dernière expression devrait être évaluée à 19.0 .

Pourquoi n'est-ce pas a // b simplement défini comme floor(a / b) ?

EDIT : 8.0 % 0.4 évalue également à 0.3999999999999996 . Au moins cela est conséquent puisque alors 8.0 // 0.4 * 0.4 + 8.0 % 0.4 évalue à 8.0

EDIT : Il ne s'agit pas d'un duplicata de Les maths à virgule flottante sont-elles cassées ? puisque je demande pourquoi cette opération spécifique est sujette à des erreurs d'arrondi (peut-être évitables), et pourquoi a // b n'est pas défini comme / égal à floor(a / b)

REMARK : Je suppose que la raison profonde pour laquelle cela ne fonctionne pas est que la division de plancher est discontinue et a donc une infinité. numéro de condition ce qui en fait un problème mal posé. La division de plancher et les nombres à virgule flottante sont fondamentalement incompatibles et vous ne devriez jamais utiliser la fonction // sur les flottants. Utilisez simplement des entiers ou des fractions à la place.

3 votes

C'est intéressant, '%.20f'%0.4 donne '0.40000000000000002220' donc 0.4 est apparemment juste un peu plus 0.4 .

2 votes

@khelwood comment floor(8.0/0.4) produisent des résultats corrects ?

1 votes

@AswinMurugesh Je ne sais pas ; c'est pourquoi je n'ai pas publié de réponse à cette question.

30voto

Borun Points 21

Comme vous et khelwood l'avez déjà remarqué, 0.4 ne peut pas être représenté exactement comme un flottant. Pourquoi ? Il s'agit de deux cinquièmes ( 4/10 == 2/5 ) qui n'a pas de représentation finie de la fraction binaire.

Essayez ça :

from fractions import Fraction
Fraction('8.0') // Fraction('0.4')
    # or equivalently
    #     Fraction(8, 1) // Fraction(2, 5)
    # or
    #     Fraction('8/1') // Fraction('2/5')
# 20

Cependant

Fraction('8') // Fraction(0.4)
# 19

Ici, 0.4 est interprété comme un littéral flottant (et donc un nombre binaire à virgule flottante) qui nécessite un arrondi (binaire), et seulement puis converti en un nombre rationnel Fraction(3602879701896397, 9007199254740992) ce qui correspond presque, mais pas exactement, à 4 / 10. Ensuite, la division flottante est exécutée, et parce que

19 * Fraction(3602879701896397, 9007199254740992) < 8.0

et

20 * Fraction(3602879701896397, 9007199254740992) > 8.0

le résultat est 19, pas 20.

Il en va probablement de même pour

8.0 // 0.4

En d'autres termes, il semble que la division flottante soit déterminée de manière atomique (mais sur les seules valeurs flottantes approximatives des littéraux flottants interprétés).

Alors pourquoi

floor(8.0 / 0.4)

donne le "bon" résultat ? Parce que là, deux erreurs d'arrondi s'annulent. Premier 1) la division est effectuée, ce qui donne quelque chose de légèrement inférieur à 20.0, mais qui n'est pas représentable en tant que valeur flottante. Il est arrondi à la valeur flottante la plus proche, qui se trouve être 20.0 . Seulement puis le floor est effectuée, mais en agissant maintenant sur exactement 20.0 et ne modifie donc plus le numéro.


1) Dans le rôle de Kyle Strand souligne que le résultat exact est déterminé puis arrondi n'est pas ce que en fait est faible 2) -(le code C de CPython ou même les instructions du CPU). Cependant, il peut s'agir d'un modèle utile pour déterminer le niveau de sécurité attendu. 3) résultat.

2) Sur le le plus bas 4) Cependant, il se pourrait que ce niveau ne soit pas trop éloigné. Certains chipsets déterminent les résultats des flottants en calculant d'abord un résultat en virgule flottante interne plus précis (mais toujours pas exact, il a simplement quelques chiffres binaires de plus), puis en arrondissant à la double précision IEEE.

3) "attendu" par la spécification Python, pas nécessairement par notre intuition.

4) Eh bien, le niveau le plus bas au-dessus de portes logiques. Il n'est pas nécessaire de tenir compte de la mécanique quantique qui rend les semi-conducteurs possibles pour comprendre cela.

2 votes

"il semble que la division en floored soit déterminée de manière atomique" -- excellente supposition, et je suppose qu'elle est correcte d'un point de vue sémantique, mais en termes de ce que l'implémentation doit faire, c'est un peu à l'envers : puisqu'il n'y a pas de support matériel qui prenne en charge l'atomique // le reste est précalculé et soustrait du numérateur afin de s'assurer que la division en virgule flottante (lorsqu'elle se produit finalement) est simplement calcule la valeur correcte immédiatement, sans avoir besoin d'un autre réglage.

1 votes

Oui, j'utilise le terme "atomique" du point de vue de l'utilisateur (c'est-à-dire du programmeur Python), ici. De la même manière que certaines opérations de base de données peuvent être décrites comme "atomiques", mais qui ne correspondent pas non plus à une seule instruction matérielle. Je parle donc de l'effet, pas de l'implémentation.

0 votes

En ce qui concerne l'implémentation, le fait que le matériel supporte ou non une instruction native équivalente à l'instruction Python // dépendrait bien sûr du matériel et des types d'opérandes. Les premiers processeurs avaient certainement un support de la division entière pour les opérandes entiers. Il se peut qu'il n'y ait pas de chipset avec un support natif pour la division flottante des flottants, mais ce ne serait pas non plus inconcevable, car ce serait simplement peu pratique, pas impossible.

11voto

shiva Points 1325

@jotasi a expliqué la vraie raison derrière cela.

Cependant, si vous voulez l'empêcher, vous pouvez utiliser decimal qui a été conçu à la base pour représenter les nombres décimaux à virgule flottante, contrairement à la représentation binaire à virgule flottante.

Donc dans votre cas vous pourriez faire quelque chose comme :

>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')

Référence : https://docs.python.org/2/library/decimal.html

0 votes

Même si ce n'est pas une réponse à une question, ce n'est pas une utilisation correcte de decimal non plus, puisque lorsque nous pouvons simplement utiliser véritable division afin d'obtenir ce résultat.

0 votes

fractions module semble faire le travail aussi bien.

0 votes

L'explication de @0x539 n'est pas vraiment correcte. Voir la réponse de jotasi et mon commentaire sous la réponse de 0x539.

10voto

0x539 Points 765

Ok, après un peu de recherche, j'ai trouvé ceci numéro . Ce qui semble se passer, c'est que, comme @khelwood l'a suggéré 0.4 évalue en interne à 0.40000000000000002220 qui, en divisant 8.0 donne quelque chose de légèrement plus petit que 20.0 . Le site / L'opérateur arrondit ensuite au nombre à virgule flottante le plus proche, ce qui revient à 20.0 mais le // tronque immédiatement le résultat, ce qui donne 19.0 .

Cela devrait être plus rapide et je suppose que c'est "proche du processeur", mais ce n'est toujours pas ce que l'utilisateur veut/attend.

7 votes

Bonne découverte, ça. Mais qu'est-ce que serait que veut un utilisateur ici ? Un comportement mathématique correct sur des nombres qui sont intrinsèquement incorrects pour commencer ? (Dont ce même "utilisateur typique" moyen est généralement béatement inconscients .)

1 votes

@RadLexus Un utilisateur veut la meilleure approximation possible pour cette opération. Dans ce cas, il s'agit de 20.0

5 votes

@0x539 : Qu'en est-il des pauvres utilisateurs qui comptent sur // pour tronquer les choses un peu moins que 20.0 à 19.0 ? Le problème ici est que l'utilisateur veut faire de l'arithmétique exacte et utilise les mauvais outils pour ce travail.

10voto

jotasi Points 2959

Après avoir vérifié les sources semi-officielles de l'objet float en cpython sur github ( https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c ) on peut comprendre ce qui se passe ici.

Pour une division normale float_div est appelé (ligne 560), ce qui convertit en interne le fichier python float s à c- double effectue la division, puis convertit le résultat en une somme de 1,5 million d'euros. double retour à un python float . Si vous faites simplement cela avec 8.0/0.4 en c vous obtenez :

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    printf("%lf\n", floor(vx/wx));
    printf("%d\n", (int)(floor(vx/wx)));
}

// gives:
// 20.000000
// 20

Pour la division du sol, il se passe autre chose. En interne, float_floor_div (ligne 654) est appelé, qui appelle ensuite float_divmod une fonction qui est censée retourner un tuple de python float contenant la division remplie, ainsi que le mod/remainder, même si ce dernier est simplement jeté par la fonction PyTuple_GET_ITEM(t, 0) . Ces valeurs sont calculées de la manière suivante (Après conversion en c- double s) :

  1. Le reste est calculé en utilisant double mod = fmod(numerator, denominator) .
  2. Le numérateur est réduit par mod pour obtenir une valeur intégrale lorsque vous effectuez ensuite la division.
  3. Le résultat pour la division flottante est calculé en calculant effectivement floor((numerator - mod) / denominator)
  4. Ensuite, la vérification déjà mentionnée dans la réponse de @Kasramvd est effectuée. Mais cela ne fait que saisir le résultat de (numerator - mod) / denominator à la valeur intégrale la plus proche.

La raison pour laquelle cela donne un résultat différent est, que fmod(8.0, 0.4) due à l'arithmétique à virgule flottante donne 0.4 au lieu de 0.0 . Par conséquent, le résultat qui est calculé est en réalité floor((8.0 - 0.4) / 0.4) = 19 et de claquer (8.0 - 0.4) / 0.4) = 19 à la valeur intégrale la plus proche ne corrige pas l'erreur introduite par le résultat "erroné" de fmod . Vous pouvez facilement faire cela en C également :

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    double mod = fmod(vx, wx);
    printf("%lf\n", mod);
    double div = (vx-mod)/wx;
    printf("%lf\n", div);
}

// gives:
// 0.4
// 19.000000

J'imagine qu'ils ont choisi cette façon de calculer la division flottante pour préserver la validité de (numerator//divisor)*divisor + fmod(numerator, divisor) = numerator (comme mentionné dans le lien de la réponse de @0x539), même si cela entraîne maintenant un comportement quelque peu inattendu de floor(8.0/0.4) != 8.0//0.4 .

1 votes

Vous semblez être la seule personne à avoir la bonne réponse. Félicitations ! Puisque vous avez dû creuser dans les sources pour le trouver, je me demande si c'est une partie obligatoire de toutes les implémentations Python ?

2 votes

Il semble qu'à partir de PEP 238 on s'attendait à ce que floor(a/b) == a // b serait vrai, puisque c'est explicitement indiqué comme la sémantique de "floor-division".

1 votes

Dans le rapport d'émission ( bugs.python.org/issue27463 ) déjà référencé par @0x539, il ne semble pas être considéré comme mauvais. et il s'agit du bugtracker python. Donc je suppose que "floor-division" est plus un nom qu'un moyen de définir l'implémentation.

8voto

Kasramvd Points 32864

C'est parce qu'il n'y a pas de 0,4 en python (représentation finie en virgule flottante) ; c'est en fait un flottant comme 0.4000000000000001 ce qui fait que le plancher de la division est de 19.

>>> floor(8//0.4000000000000001)
19.0

Mais la véritable division ( / ) renvoie une approximation raisonnable du résultat de la division si les arguments sont des flottants ou des complexes. Et c'est pourquoi le résultat de 8.0/0.4 est de 20. Cela dépend en fait de la taille des arguments (en C les arguments doubles). ( pas d'arrondi au flottant le plus proche )

Plus d'informations sur pythons integer division floors par Guido lui-même.

Pour des informations complètes sur les numéros de flotteurs, vous pouvez également lire cet article. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Pour ceux qui s'y intéressent, la fonction suivante est la suivante float_div qui effectue la véritable tâche de division pour les nombres flottants, dans le code source de Cpython :

float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

dont le résultat final serait calculé par la fonction PyFloat_FromDouble :

PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        free_list = (PyFloatObject *) Py_TYPE(op);
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}

0 votes

Kasramvd Merci pour cette réponse détaillée. Je suis peut-être un peu lourd, mais je ne comprends pas ce que vous voulez dire par "snapping to next integral value". De toute évidence, toutes les divisions en virgule flottante ne seront pas arrondies à la prochaine valeur intégrale ( 3./4. ne donnera pas 1 ). Par conséquent, la décision ne peut pas être aussi simple que vous le présentez, d'après ce que je comprends. Vous ai-je bien compris ?

1 votes

En fait, après avoir vérifié le code source moi-même, je suppose que la division en virgule flottante est effectuée dans la fonction float_div alors que float_divmod n'est appelé que par float_floor_div qui fait la division par le sol, ce qui donne un résultat "erroné". 19 au lieu de 20 .

0 votes

@jotasi Oui, exactement. C'est plus compliqué qu'un simple snapping. Et oui, c'est float_div qui effectue la véritable tâche de plongée. Il semble qu'elle calcule le résultat final en quelque sorte en fonction de la taille des arguments. J'ai mis à jour la réponse. Merci pour votre attention.

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