Есть два варианта работы с числами: по правилам математики и правилам компьютера.
В реальном мире используются математические правила: в финансах, исследованиях, подсчётах товаров при походе в магазин.
Компьютерный машинный язык отличается, потому что данные там хранятся как удобно компьютеру, а не человеку — в двоичной системе счисления (0 и 1). Из-за этого некоторые числа в коде становятся непохожими на себя. Например, посмотрите на такой Python-код:
x = sum([0.1] * 1000)
print('Ошибка: [0.1] * 1000 =', x)
Даст такой результат:
Ошибка: [0.1] * 1000 = 99.9999999999986
Так происходит из-за особенностей хранения десятичных дробей в памяти компьютера. Десятичные дроби хранятся в Python в формате float, и у этого формата есть особенности.
Во-первых, двоичные числа будут представлены точно, только если это степени двойки, например число 0,5 будет без отклонений, потому что это 1/2. Остальные числа будут иметь погрешности, как в примере выше: эти погрешности видны, если вывести число с представлением большого количества символов после запятой.
Математические операции с этими числами могут давать непредсказуемые результаты, которые будут отличаться от реальной математики.
Во-вторых, для формата float максимальное число, которое можно представить точно, — 2 в степени 53, то есть 9007199254740992. После этого шаг для возможного представления чисел начинает увеличиваться. Следующее число, которое может быть сохранено в памяти, будет 9007199254740994, то есть весь большой диапазон чисел между этими двумя округляется до ближайшего из них. В итоге число 9007199254740993 мы потеряли :-(
Математические правила округления
Когда мы говорим про обычное округление, то чаще всего имеем в виду округление дробного числа до ближайшего целого и вверх. Это работает так:
- Если дробная часть меньше 0,5, округляем вниз. Округлённое число 2,4 равно 2.
- Если дробная часть больше или равна 0,5, округляем вверх. Все округлённые числа в диапазоне от 2,5 до 3 будут равны 3.
Получается, что большая часть дробных чисел всегда округляется в большую сторону:
Стандартное округление в Python
В Python стандартное округление работает не как обычное, а как банковское. Это значит, что десятичный остаток будет дополнен или уменьшен до ближайшего чётного числа.
Округлённое число 2,5 будет равно 2, то есть округление сработает в обратную сторону. Округление 3,5 сработает как обычное, то есть до 4.
Зачем округлять числа
Без округления в реальной жизни не обойтись. Математические операции могут выдавать дробные числа даже в тех отраслях, где это не нужно.
Например, деление 10 на 3 даст дробное число, которое не имеет точного представления. Если дальше использовать этот результат без округления, такие погрешности могут накапливаться и искажать финальный результат вычислений — например, банковский вклад или стоимость акции после начисления процентов через несколько лет.
Ещё одна причина для округления — ограничение дробной части валют в финансах. Почти любая валюта может иметь максимум два дробных числа после запятой в стандартных операциях. Например, 100,55 рубля означает 100 рублей 55 копеек, а 10,99 доллара будет означать 10 долларов 99 центов. У некоторых валют есть только целые части, например у японских йен.
Необходимость округления вызвана не только сложными практическими причинами, но и просто потому, что так проще работать. Вот ещё несколько примеров.
- Аналитические графики и отчёты лучше воспринимаются с округлёнными показателями до привычных двух цифр после запятой. Ещё лучше — с целыми числами.
- В математике числа округляются для упрощения и ускорения исследований.
- В инженерном строительстве нет никакого смысла использовать совсем мелкие дробные числа. Если при строительстве самолёта важны даже малейшие погрешности, то при строительстве дома обычно достаточно округления до сантиметров и миллиметров.
- В медицине округление в большую или меньшую сторону может использоваться при дозировке для страховки, чтобы избежать ошибок при введении или приёме.
Почему важно
Во всех современных языках программирования есть несколько вариантов округления чисел, и все они будут влиять на итоговый результат немного по-разному, то есть встраивать смещение результата разного масштаба в зависимости от того, какой инструмент используется.
Ошибки округления становились причиной скандалов на фондовых биржах, неправильных итогов политических выборов и даже космических катастроф. Поэтому подходить к выбору инструмента для работы с числами нужно ответственно.
Вот какие варианты округления чисел есть на Python.
Функция round() в Python
Стандартная функция округления в Python.
Чтобы дать команду округлить число, нужно указать это число в скобках после слова round. Так мы округлим переменную x:
x = round(x)
Функция round() работает по принципу банковского округления. Получается, что если в переменной x будут храниться числа 1,5 и 2,5, округлённое значение в обоих случаях будет одинаковым — 2, потому что это ближайшее чётное число.
Round можно настроить, указав количество знаков после запятой. Формула настройки этой функции выглядит так:
round(number[, ndigits])
Работает это так:
- round — название функции.
- number — число, которое мы хотим округлить.
- ndigits — количество символов после запятой для округления number. Если ничего не указывать,
round()будет работать как обычное банковское округление.
К дробной части числа тоже применяется банковское округление. Получив такую команду:
round(123.456789, 2)
…Python округлит число до 123,46 (потому что после ,45 идёт 6). А вот round(123.4564789, 2) округлится уже до 123,45.
При этом из-за особенностей двоичной системы некоторые числа, дробная часть которых оканчивается на 5, на самом деле получаются немного больше, чем представлены на экране. Если вывести переменную x = 2.85 с 30 символами после запятой, мы увидим:
2.850000000000000088817841970013
Поэтому иногда округление должно довести число до ближайшего чётного вниз (в нашем примере 2,8), но фактически округляет вверх (у нас получится 2,9).
Функция int() в Python
Метод приведения к целому числу, который просто отбрасывает всю дробную часть:
- Округление
int(10.1)будет равно 10. - Округление
int(10.5)будет равно 10. - Округление
int(10.99)будет равно 10.
Это грубый, но удобный способ, если мы точно знаем, какой результат получим.
Функцию можно использовать для усечения до нужного количества знаков после запятой, если по каким-то причинам необходимо использовать именно int(). Но делать это нужно осторожно, потому что int() может серьёзно искажать реальную картину.
Проведём опыт и посмотрим, что может получиться, если округлять числа этим способом. Создадим имитацию биржевых колебаний. Возьмём две одинаковые исходные переменные, равные 100, — это будет наш капитал. Будем изменять эти переменные в течение 1 000 000 виртуальных дней. В нашей программе каждый день стоимость капитала будет меняться случайным образом вверх или вниз, но не более чем на 5% от предыдущего значения. Каждый виртуальный день программа будет возвращать обновлённое значение капитала.
Одну переменную оставим без округления — это будет usual_number.
Вторую переменную будем округлять до трёх знаков после запятой. Для этого сначала будем умножать результат после изменения числа на 1 000, отбрасывать дробную часть функцией int() и разделять на 1 000, чтобы вернуть десятичный остаток до трёх знаков.
Запустим цикл изменений 1 000 000 раз и посмотрим, что получилось.
Программа:
# создаём генератор случайных чисел
import random
random.seed(100)
# пишем функцию для округления до трёх знаков после запятой
def truncate(n):
return int(n * 1000) / 1000
# объявляем две переменные
usual_number, truncated_number = 100, 100
# запускаем цикл изменений переменных 1 000 000 раз
for i in range(1000000):
# возвращаем случайное число из диапазона от −5% до 5%
delta = random.uniform(-0.05, 0.05)
# Обновляем значения переменных
usual_number += delta
truncated_number = truncate(truncated_number + delta)
# выводим результат на экран после 1 000 000 повторений цикла
print('Результат без округления:', usual_number)
print('Результат с округлением:', truncated_number)
Результат:
Результат без округления: 96.45273913513529
Результат с округлением: 0.239
Видно, что функция с int() показывает совсем не то, что получается на самом деле.
Теперь посмотрим, что будет, если вместо нашей функции truncated() использовать стандартное банковское округление round() с округлением до трёх знаков:
Результат без округления: 96.45273913513529
Результат с округлением: 96.258
Результат получается очень близким к неокруглённому, то есть банковское округление действительно работает довольно точно.
Модуль math в Python
Ещё один способ округления в Python — встроенная библиотека math. Подключается в проект она командой import math, которую нужно написать в начале скрипта.
Плюс этого варианта в том, что мы можем точно сказать, в какую сторону нужно округлять число.
Math.ceil округляет число вверх:
# результат всех этих операций: 15
math.ceil(14.01)
math.ceil(14.5)
math.ceil(14.99)
# результат всех этих операций: −15
math.ceil(-15.01)
math.ceil(-15.5)
math.ceil(-15.99)
Math.floor округляет число вниз:
# результат всех этих операций: 15
math.ceil(15.01)
math.ceil(15.5)
math.ceil(15.99)
# результат всех этих операций: −15
math.ceil(-14.01)
math.ceil(-14.5)
math.ceil(-14.99)
Math.trunc просто отсекает дробную часть, то есть работает в точности как функция int().
Модуль decimal в Python
Модуль, который помогает решить проблемы с неправильным округлением из-за особенностей представления чисел в двоичной системе.
Для использования в начале скрипта decimal нужно импортировать командой:
from decimal import Decimal
После этого создаётся переменная, которая будет объектом этого модуля:
D = Decimal
Теперь в новый объект можно передать число и указать количество знаков после запятой для округления. Количество знаков указывается при помощи метода quantize:
D(“0.444”).quantize(D(“1.0000”))
Разберём, как это работает.
- Сначала указываем наш объект и в скобках передаём число, которое указываем в кавычках:
D(“0.444”). - После этого ставим точку и вызываем метод
.quantize. - В скобках после метода указываем наш объект и в скобках после объекта — количество знаков для округления. Для этого сначала ставим 1, потом точку, потом ставим нужное количество нулей — это будут наши знаки после запятой:
quantize(D(“1.0000”)).
С объектами decimal можно использовать целые числа типа int, но не дробные типа float. То есть к нашей переменной D можно прибавить 10, но нельзя 10,1.
Округление decimal тоже можно настраивать. Для этого используется ещё один параметр, который указывается через запятую в скобках у метода quantize, например:
D(“0.550001”).quantize(D(“1.00”), decimal.ROUND_CEILING)
У decimal больше вариантов настройки округления, чем у math:
decimal.ROUND_CEILING— округление вверх, в сторону положительной бесконечности;decimal.ROUND_DOWN— округление к нулю;decimal.ROUND_FLOOR— округление к отрицательной бесконечности;decimal.ROUND_HALF_DOWN— округляет вверх от нуля, если остаток после округляемых знаков больше 0,5, иначе округляет к нулю (например, 0,445 будет округлено до 0,44, если нам нужно два знака после запятой);decimal.ROUND_HALF_EVEN— работает так же, но если следующая цифра после нужного количества знаков равна 5, округляет к ближайшему чётному числу;decimal.ROUND_HALF_UP— обычное математическое округление, которое округляет к ближайшему числу (если дробная часть равна 5, округляет вверх);decimal.ROUND_UP— всегда округляет в большую сторону;decimal.ROUND_05UP— округляет вверх, только если последняя цифра перед округлением — 5, иначе ведёт себя как округление вниз.
Как в итоге округляют важные числа
Хотя стандартное округление round подходит для большинства задач, при работе с чувствительными данными — например, финансами или исследованиями в Data Science — могут потребоваться более точные настройки. Поэтому в серьёзных программах используется decimal и его методы.
