Как избежать ошибок с плавающей запятой?

Я пытался написать функцию для аппроксимации квадратного корня (я знаю, что есть математический модуль ... Я хочу сделать это сам), и меня обманула арифметика с плавающей запятой. Как этого избежать?

def sqrt(num):
    root = 0.0
    while root * root < num:
        root += 0.01
    return root

Использование этого дает следующие результаты:

>>> sqrt(4)
2.0000000000000013
>>> sqrt(9)
3.00999999999998

Я понимаю, что могу просто использовать round(), но я хочу сделать это действительно точным. Я хочу иметь возможность вычислять до 6 или 7 цифр. Это было бы невозможно, если бы я округлялся. Я хочу понять, как правильно обрабатывать вычисления с плавающей запятой в Python.


person temporary_user_name    schedule 20.10.2013    source источник
comment
Возможно, попробуйте модуль decimal, который разработан для обеспечения точности?   -  person Michael0x2a    schedule 20.10.2013


Ответы (2)


На самом деле это не имеет ничего общего с Python - вы бы увидели такое же поведение на любом языке, используя двоичную арифметику с плавающей запятой вашего оборудования. Сначала прочтите документы.

Прочитав это, вы лучше поймете, что вы не добавляете одну сотую в свой код. Это именно то, что вы добавляете:

>>> from decimal import Decimal
>>> Decimal(.01)
Decimal('0.01000000000000000020816681711721685132943093776702880859375')

Эта строка показывает точное десятичное значение приближения двоичного числа с плавающей запятой («двойная точность» в языке C) к точному десятичному значению 0,01. То, что вы действительно добавляете, немного больше 1/100.

Управление числовыми ошибками с плавающей запятой - это область, называемая «численный анализ», и это очень большая и сложная тема. Если вас поразит тот факт, что числа с плавающей запятой являются всего лишь приближением к десятичным значениям, используйте модуль decimal. Это избавит вас от множества «мелких» проблем. Например, учитывая эту небольшую модификацию вашей функции:

from decimal import Decimal as D

def sqrt(num):
    root = D(0)
    while root * root < num:
        root += D("0.01")
    return root

тогда:

>>> sqrt(4)
Decimal('2.00')
>>> sqrt(9)
Decimal('3.00')

Он не совсем точен, но может быть менее удивительным в простых примерах, потому что теперь он добавляет ровно одну сотую.

Альтернативой является использование чисел с плавающей запятой и добавление чего-то, что точно может быть представлено в виде двоичных значений с плавающей запятой в форме I/2**J. Например, вместо 0,01 добавьте 0,125 (1/8) или 0,0625 (1/16).

Тогда найдите "Метод Ньютона" для вычисления квадратных корней ;-)

person Tim Peters    schedule 20.10.2013
comment
Для записи я прочитал документацию и уже знал обо всем, что касается точности вычислений с плавающей запятой при хранении двоичных представлений. Я забыл метод Ньютона. Вы здесь отвечаете на все мои вопросы! Мой счастливый день, когда вы открыли для себя SO. Интересно, как работает модуль Decimal. Есть ли какой-нибудь способ узнать это, кроме чтения первоисточника? - person temporary_user_name; 20.10.2013
comment
Ну, decimal изначально был написан на Python и работал со списками десятичных цифр (0, 1, 2, ..., 9). Очень похоже на то, как мы делаем арифметику на бумаге! Плавающая точка просто требует добавления (десятичной) экспоненты к представлению, а затем быть очень осторожным ;-) Текущий модуль decimal закодирован на C, и он гораздо менее понятен :-( - person Tim Peters; 20.10.2013
comment
как вы сказали, я пытался решить 4 - 3.2, используя десятичный модуль. a = десятичный (4) b = десятичный (3.2), но результат a - b является десятичным ('0,7999999999999998223643160600') - person Srinesh; 05.01.2016
comment
@Srinesh - Попробуйте Decimal('4') - Decimal('3.2'). - person Robᵩ; 26.07.2016

То есть есть такие модули, как decimal и fractions. Но я создал класс для подобных задач. Этот класс решает только сложение, вычитание, умножение, деление, деление и модуль. Но его легко расширить. Он в основном преобразует числа с плавающей запятой в список ([число с плавающей запятой, степень десяти для умножения числа с плавающей запятой, чтобы получить целое число]) и оттуда выполняет арифметические операции. Целые числа более точны, чем числа с плавающей запятой в python. Вот чем пользуется этот класс. Итак, без лишних слов, вот код:

class decimal():
    # TODO: # OPTIMISE: code to maximize performance
    """
    Class decimal, a more reliable alternative to float. | v0.1
    ============================================================
            Python's floats (and in many other languages as well) are
    pretty inaccurate. While on the outside it may look like this:

    .1 + .1 + .1

            But on the inside, it gets converted to base 2. It tells
    the computer, "2 to the power of what is 0.1?". The
    computer says, "Oh, I don't know; would an approximation
    be sufficient?"
    Python be like, "Oh, sure, why not? It's not like we need to
    give it that much accuracy."
            And so that happens. But what they ARE good at is
    everything else, including multiplying a float and a
    10 together. So I abused that and made this: the decimal
    class. Us humans knows that 1 + 1 + 1 = 3. Well, most of us
    anyway but that's not important. The thing is, computers can
    too! This new replacement does the following:

            1. Find how many 10 ^ n it takes to get the number inputted
                    into a valid integer.
            2. Make a list with the original float and n (multiplying the by
                    10^-n is inaccurate)

            And that's pretty much it, if you don't count the
    adding, subtracting, etc algorithm. This is more accurate than just
    ".1 + .1 + .1". But then, it's more simple than hand-typing
    (.1 * 100 + .01 * 100 + .1 * 100)/100
    (which is basically the algorithm for this). But it does have it's costs.
    --------------------------------------------------------------------------

    BAD #1: It's slightly slower then the conventional .1 + .1 + .1 but
        it DOES make up for accuracy

    BAD #2: It's useless, there are many libraries out there that solves the
            same problem as this. They may be more or less efficient than this
            method. Thus rendering this useless.
    --------------------------------------------------------------------------
    And that's pretty much it! Thanks for stopping by to read this doc-string.
    --------------------------------------------------------------------------
        Copyright © 2020 Bryan Hu

        Permission is hereby granted, free of charge, to any person obtaining
        a copy of this software and associated documentation files
        (the "Software"), to deal in the Software without restriction,
        including without limitation the rights to use, copy, modify,
        merge, publish, distribute, sub-license, and/or sell copies of
        the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included
        in all copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
        OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
        IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
        CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
        TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
        SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    """

    def __init__(self, number):
        super(decimal, self).__init__()
        if number is iter:
            processed = float(number[0])
        else:
            processed = float(number)
        x = 10
        while round(processed * x) != processed * x:
            x *= 10
        self.number = [processed, x]

    def __add__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                (num[0] * maximum + the_other_number[0] * maximum) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
             is not a valid type".format(
                other, type(other))

    def __float__(self):
        return float(self.number[0])

    def __bool__(self):
        return bool(self.number[0])

    def __str__(self):
        return str(self.number)

    def __iter__(self):
        return (x for x in self.number)

    def __repr__(self):
        return str(self.number[0])

    def __sub__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                (num[0] * maximum - the_other_number[0] * maximum) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
         is not a valid type".format(
                other, type(other))

    def __div__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                ((num[0] * maximum) / (
                    the_other_number[0] * maximum)) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
         is not a valid type".format(
                other, type(other))

    def __floordiv__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                ((num[0] * maximum) // (
                    the_other_number[0] * maximum)) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
         is not a valid type".format(
                other, type(other))

    def __mul__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                ((num[0] * maximum) * (
                    the_other_number[0] * maximum)) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
         is not a valid type".format(
                other, type(other))

    def __mod__(self, other):
        the_other_number, num = list(other), list(self.number)
        try:
            maximum = max(
                float(num[1]), float(the_other_number[1]))
            return decimal(
                ((num[0] * maximum) % (
                    the_other_number[0] * maximum)) / maximum)
        except IndexError:
            raise "Entered {}, which has the type {},\
         is not a valid type".format(
                other, type(other))
    # Pastebin: https://pastebin.com/MwzZ1W9e
person theX    schedule 13.06.2020
comment
Я тестировал это только с помощью простых вещей, таких как .1 + .1 + .1 vs decimal(.1) + decimal(.1) + decimal(.1), но теоретически все поддерживаемые арифметические типы тоже должны работать. - person theX; 13.06.2020
comment
Эй, я ценю твои усилия! Надеюсь, это было вам полезно. Но я бы никогда не рекомендовал самодельный и в основном непроверенный фрагмент кода в качестве замены стандартизированных модулей и библиотек, которые обрабатывают чувствительный аспект программных вычислений. - person temporary_user_name; 13.06.2020
comment
Знаю, но надеюсь, что в будущем это пригодится. - person theX; 13.06.2020