Есть два варианта работы с числами: по правилам математики и правилам компьютера.
В реальном мире используются математические правила: в финансах, исследованиях, подсчётах товаров при походе в магазин.
Компьютерный машинный язык отличается, потому что данные там хранятся как удобно компьютеру, а не человеку — в двоичной системе счисления (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 и его методы.