Несколько лет назад, будучи преподавателем курса по анализу данных в Python, я подводил итоги курса и рассчитывал итоговые оценки студентов. Так как курс был об анализе данных и использовал я на нём в основном Python, все расчёты также были сделать в нём.
Сделал я быстренько все вычиления и отправил их студентам итоговые оценки вместе с кодом расчёта, чтобы каждый, кто приобрел хоть минимальные знания на курсе, мог бы проверить неоспоримую истинность тех оценок. А так как я никогда не задрачивал студентов и оценивал все работы очень лояльно, если не сказать пофигистично, то едва ли можно было ожидать, чтобы кто-то оспорил свой итоговых балл.
Но я недооценивал студентов Питерской вышки. Благодаря своему занудству, перед нашим расставанием они успели поведать мне об одной особенности Питона, о которой я, да и многие другие опытные разработчики, не подозревали. Это особенно удивительно, ведь с этой особенностю приходится постоянно иметь дело. Речь идёт об особом способе округления чисел в Python, который обнаружилася, когда студенты пересчитали средние баллы в Excel и получили отличные от моих результаты. Они же первые и смогли объяснить причины различий.
Итак, оказалось, что округление — не такая простая тема, как может показаться. Говоря об округлении, чаще всего имеют ввиду округление к ближайшему целому. Оно производится по довольно простому правилу — если последнее число перед отсекаемым меньше 5, то округление происходит в меньшую сторону, а если оно >= 5, то в большую. По этому правилу, например 1.1, 1.2, 1.3 и 1.4 округляются до 1, а всё, что больше или равно 1.5 и меньше 2 округляется до 2. Именно так, кстати, работает округление в Excel, да и много где ещё.
Давайте проверим, что будет в Python:
round(1.4), round(1.5), round(1.6) # всё верно
Казалось бы, ничего не предвещает беды — 1.5 округляется к 2, а 1.4 к 1. Логично также предположить, что и для других чисел ничего не изменится
round(2.4), round(2.5), round(2.6)
LOLWUT? Почему 2.5 округлилось в меньшую сторону? Знакомьтесь — это банковское округление (bankers rounding) то есть округление к ближайшему чётному, которое в Python присутствует везде.
print(f""" В строках округление будет происходить по тому же принципу. Ближайщее чётное: {1.25:.1f} Ближайщее нечётное: {1.35:.1f} """)
Банковским оно называется потому, что часто использовалось банкирами, так как удовлетворяло их специфичную потребность, относящуюся, скорее, не к математике, а к реальному миру. Дело в том, что в магазинных ценниках цена часто уже округлена до чисел, кратных пяти, например, до половины рубля (доллара, лари...). Согласитесь, гораздо чаще можно встретить цену в 2.5 доллара, чем в 2.39 или 2.71. Если округлять подобные цены по правилам ближайшего целого, то уже через 100 таких округлений, которые будут встречаться довольно часто, накопится погрешность в 50 единиц. Банковское округление исключает такой вариант, чередуя округление в большую и меньшую сторону в зависимости от чётности/нечётности числа, стоящего перед округляемым.
Не совсем, правда, понятно, почему для языка общего назначения был выбран именно такой способ округления, тем более что в других известных мне языках используется обычное округление к ближайшему целому, включая предыдущию версию Python 2. Если кто знает причины выбора такого способа округления, напишите, пожалуйста, в комментариях.
Для полноты раскрытия темы стоит упомянуть, что при помощи модуля decimal из стандартной библиотеки можно выбирать любой из восьми способов округления на свой вкус.
import decimal decimal.Decimal('2.5').quantize( decimal.Decimal('1'), rounding=decimal.ROUND_HALF_UP )
from math import floor, ceil floor(2.5), ceil(2.5)
А мораль истории этой истории в том, что не всегда преподаватель учит студентов, иногда бывает наоборот.
Комментарии