Неправильная арифметика с вещественными числами. Простой пример

в 16:43, , рубрики: python, арифметика, Программирование, метки: ,

Все программисты(или почти все) знают как представляются примитивные типы(целые, вещественные числа и т.п.) в памяти компьютера. Мы знаем, как выполняются операции с ними и в каких случаях могут произойти проблемы, такие как переполнение. Но при написании конкретного кода, особенно на языках высокого уровня, и тем более, скриптовых языках, мы не всегда это помним.
Любой, кто занимался разработкой биллинга или просто хранил денежную сумму в памяти знает, что её нужно хранить в целочисленном формате, а не вещественном, но при работе с другими объектами об этом вспоминают не всегда.

Но, конечно, в первую очередь этот пост для новичков. Думаю это не плохой пример для тех, кто утверждает «зачем мне знать как работают запросы/устроенны потоки/выполняются вычисления/..., если все это делает за меня компилятор и ОС!»

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

class Member:
    def __init__(self, name="", first_result=0, second_result=0):
        self.name = name
        self.first_result = first_result
        self.second_result = second_result
        self.final_result = 0
        self.position = 0
    # Вывод информации об участнике
    def __repr__(self):
        return "n" + str(self.position) + " "*10 + str(self.final_result) + " "*10 + self.name

def proccess_raiting(members, first_koeff, second_koeff):
        #Подсчет результатов
        for member in members:
            member.final_result = member.first_result*first_koeff + member.second_result*second_koeff
        #Сортируем по итоговому результату
        members.sort(key=lambda x: x.final_result)

        #Расставляем места
        k = 0
        prev_member = None
        for member in members:
            if prev_member:
                #Если результаты не равны, то следующий участник занимает на 1 позицию ниже, чем предыдущий
                if prev_member.final_result != member.final_result:
                    k += 1
            else:
                k +=1
            member.position = k
            prev_member = member
        return members


#Проверим работу скрипта:
members = [Member("Ivan", 101.1, 50.2), Member("Alexander", 101.1, 58.2), Member("Vladimir", 101.5, 49.6)]

print proccess_raiting(members, 3, 2)

И результат будет НЕ правильный:

>>>[
1          403.7          Ivan,
2          403.7          Vladimir,
3          419.7          Alexander]

Посмотрим, почему так произошло, вычислив final_result отдельно:

>>> 101.1*3 + 50.2*2
403.69999999999993
>>> 101.5*3 + 49.6*2
403.7

Очевидно, что

>>>101.1*3 + 50.2*2==101.5*3 + 49.6*2
False

Проблема возникает из-за того, что некоторые десятичные числа в двоичной системе имеют вид бесконечной(или очень длинной) двоичной дроби. Поэтому такие числа не могут быть точно представлены в вещественных переменных

image

То, что эта ошибка проявляется не для всех вещественных чисел только усложняет процесс её поиска и уменьшает вероятность её обнаружить при тестировании.

Возможные решения на языке Python:
Вариант 1.
Использование типа Decimal

        Member("Ivan", Decimal('101.1'), Decimal('50.2'))

Вариант 2.
Округление результатов:

    member.final_result = round(member.first_result*first_koeff + member.second_result*second_koeff, 5)

P.S. Подробнее о представлении вещественных чисел в памяти и проблемах связанных с этим, можно прочитать здесь habrahabr.ru/post/112953/

Автор: DemonZX

Источник

  1. Александр:

    Потому что этот ваш питон не приспособлен для бухгалтерских вычислений.
    В финансовых расчётах не должно быть бесконечных дробей и вообще, после запятой надо брать не больше четырёх знаков, else при больших объемах расчётов больших сумм появится возможность жульничества или накопление ошибки. Надо округлять последний знак.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js