Python: от данных до своей сводки Урок 36 из 56
Числа в отчёте: форматы, округление и единицы
Тридцать пятый урок курса по Python. `1234567.891` и `1 234 567,89` — одно число и два разных отчёта. Разбираем формат-строки, разделители по-нашему, `round`, который округляет к чётному, `Decimal` для денег и сумму округлённых, которая не равна округлённой сумме.
Зачем это нужно
Отчёт собран, страница открывается, а в ячейке стоит 11.539999999999999. Или 1234567.891 — число, которое читатель будет считать глазами по три цифры, и всё равно ошибётся.
Форматирование — это не украшение, а последний шаг работы с данными: всё, что выше, считает с полной точностью, а здесь число превращается в текст для человека. И ровно здесь живут три ловушки, из-за которых отчёт перестаёт сходиться.
Сразу целиком
Файл chisla.py.
"""Урок 35: числа в отчёте.
Одно и то же число, показанное семью способами, и три ловушки округления,
из-за которых отчёт перестаёт сходиться.
"""
from decimal import Decimal, ROUND_HALF_UP
amount = 1234567.891
print("== одно число, семь видов")
print("как есть: ", amount)
print("два знака: ", f"{amount:.2f}")
print("с разделителем: ", f"{amount:,.2f}")
print("по-нашему: ", f"{amount:,.2f}".replace(",", " ").replace(".", ","))
print("в миллионах: ", f"{amount / 1_000_000:.1f} млн")
print("со знаком: ", f"{amount:+,.0f}")
print("в колонке шириной 16:", f"|{amount:>16,.2f}|")
print()
print("== доля: процент — это формат, а не умножение")
share = 0.1274
print("руками:", round(share * 100, 1), "% | форматом:", f"{share:.1%}")
print()
print("== ловушка первая: round округляет к чётному")
for value in (0.5, 1.5, 2.5, 3.5):
print(f" round({value}) = {round(value)}")
print(" это не ошибка, а правило: половинки идут к чётному")
print()
print("== ловушка вторая: для денег берут Decimal")
money = Decimal("2.675")
print(" round(2.675, 2) =", round(2.675, 2), "— двоичная дробь не равна 2.675")
print(" Decimal с ROUND_HALF_UP =", money.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
print()
print("== ловушка третья: сумма округлённых ≠ округлённая сумма")
rows = [10.005, 10.005, 10.005, 10.005]
rounded = [round(value, 2) for value in rows]
print(" строки:", rounded)
print(" сумма округлённых:", round(sum(rounded), 2))
print(" округлённая сумма:", round(sum(rows), 2))
print(" расхождение:", round(sum(rows) - sum(rounded), 2))
Выводит:
== одно число, семь видов
как есть: 1234567.891
два знака: 1234567.89
с разделителем: 1,234,567.89
по-нашему: 1 234 567,89
в миллионах: 1.2 млн
со знаком: +1,234,568
в колонке шириной 16: | 1,234,567.89|
== доля: процент — это формат, а не умножение
руками: 12.7 % | форматом: 12.7%
== ловушка первая: round округляет к чётному
round(0.5) = 0
round(1.5) = 2
round(2.5) = 2
round(3.5) = 4
это не ошибка, а правило: половинки идут к чётному
== ловушка вторая: для денег берут Decimal
round(2.675, 2) = 2.67 — двоичная дробь не равна 2.675
Decimal с ROUND_HALF_UP = 2.68
== ловушка третья: сумма округлённых ≠ округлённая сумма
строки: [10.01, 10.01, 10.01, 10.01]
сумма округлённых: 40.04
округлённая сумма: 40.02
расхождение: -0.02
Разбор
Формат-строка: что стоит после двоеточия
f"{amount:,.2f}" — внутри фигурных скобок после двоеточия описано, как показать число:
| Запись | Что делает |
|---|---|
.2f |
два знака после точки, всегда |
, |
разделитель тысяч |
+ |
знак даже у положительного |
>16 |
выравнивание вправо по ширине 16 |
.1% |
доля как процент: 0.127 → 12.7% |
,.0f |
целое с разделителем |
Всё это — часть языка, а не библиотека, и работает одинаково в print, в f-строке и в format().
Разделители: у нас не как по умолчанию
Python по умолчанию пишет 1,234,567.89 — запятая в тысячах, точка в дробях. У нас наоборот: пробел в тысячах, запятая в дробях. Отсюда приём из примера:
f"{amount:,.2f}".replace(",", " ").replace(".", ",")
Порядок важен: сначала запятые становятся пробелами, потом точка — запятой. Наоборот получится каша.
Пробел лучше брать неразрывный (\u00a0): обычный пробел браузер переносит на другую строку, и 1 234 может разъехаться по разным строчкам таблицы.
Есть модуль locale, который умеет это сам, но он настраивается на весь процесс сразу и зависит от того, какие локали стоят в системе, — в программе, которая работает на сервере, это чаще мешает, чем помогает. Три replace честнее.
Главное правило: единица измерения живёт в заголовке столбца, а не в каждой ячейке. сумма, ₸ сверху и чистые числа под ним читаются, а 1 240 500,00 ₸ в каждой строке — нет.
round округляет к чётному
Первая ловушка, и она не ошибка Python: round(0.5) даёт 0, round(2.5) даёт 2, а round(1.5) и round(3.5) — 2 и 4. Половинки уходят к чётному, и это стандарт: при большом количестве округлений ошибка не накапливается в одну сторону.
Если нужно школьное «половина вверх», это Decimal с ROUND_HALF_UP.
Деньги — Decimal, а не float
round(2.675, 2) даёт 2.67, хотя ждали 2.68. Виноват не round, а то, что 2.675 в двоичной дроби — это чуть меньше, чем 2.675 (четвёртый урок).
Для денег берут Decimal("2.675") — от строки, не от числа, — и quantize с явным правилом округления. Правило простое: деньги считают в Decimal или в целых тиынах, а float оставляют физике и статистике.
Сумма округлённых ≠ округлённая сумма
Третья ловушка — та, из-за которой в отчёте «не сходится копейка». Четыре строки по 10.005: каждая округляется до 10.01, сумма округлённых — 40.04, а округлённая сумма — 40.02.
Что с этим делают:
- Считают по неокруглённым, а округляют только при выводе — тогда итог верный, но столбец в него не складывается на глаз;
- либо подгоняют последнюю строку: разницу дописывают в самую большую строку, чтобы столбец складывался;
- и в любом случае пишут в отчёте, что именно вы сделали, если расхождение может увидеть читатель.
Чего делать нельзя — считать итог по округлённым строкам и называть это суммой.
Масштаб: когда 1.2 млн лучше, чем 1 234 567
Крупные числа в отчёте для человека обычно показывают в тысячах или миллионах: 1.2 млн ₸ читается с одного взгляда, 1 234 567 ₸ — нет. Но в таблице, по которой будут сверять или складывать, оставляют полное число: округление до миллионов прячет как раз ту разницу, ради которой таблицу открыли.
Простое правило: в тексте и заголовках — крупно, в таблице — точно.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
round(2.5)даёт2, и ошибка ли это? - Почему сумма округлённых строк может не совпасть с округлённой суммой?
- Где должна стоять единица измерения — в ячейке или в заголовке столбца?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
amount = 45678.4
print(f"{amount:.0f}")
print(f"{amount:,.2f}")
print(f"{amount:+.1f}")
print(f"|{amount:>12,.1f}|")
print(f"{amount / 1000:.1f} тыс.")
2. Заполните пропуск. Вместо ... приведите число к местному виду: пробел в тысячах, запятая в дробях.
# по умолчанию Python пишет 1,234,567.89
amount = 1234567.891
print(f"{amount:,.2f}"...)
3. Почините. Доля напечаталась как 1274.0%.
# процент — это формат, и он умножает сам
share = 0.1274
print(f"доля: {share * 100:.1%}")
Задание
Обязательное. Напечатайте таблицу расходов: статья, сумма, доля и изменение. Суммы — в местном виде с двумя знаками, доли — процентом с той же запятой, изменения — со знаком, а ноль изменения — прочерком. Итог посчитайте по неокруглённым значениям. В конце напечатайте проверку: сходится ли итог, чему равна сумма округлённых строк и на сколько она расходится с настоящей.
Ожидаемый вывод:
статья сумма, ₸ доля изменение, ₸
аренда 1 240 500,00 67,4% 62 500,00
еда 348 210,34 18,9% -12 400,00
топливо 190 880,67 10,4% 4 300,00
связь 59 941,00 3,3% —
итого 1 839 532,00 100,0%
проверка:
итог посчитан до округления: True
сумма округлённых строк: 1 839 532,01
округлённый итог: 1 839 532,00
расхождение: 0.01
доли складываются в: 100,0%
Готово, когда: вывод совпадает построчно; форматирование вынесено в функции, а не повторяется в каждой строке; итог считается из исходных чисел; расхождение посчитано и напечатано, а не спрятано.
На своих данных. Возьмите любую свою таблицу с деньгами и напечатайте её дважды: с итогом по неокруглённым и с итогом по округлённым строкам. Если числа сошлись — увеличьте число строк, и они разойдутся. Решите заранее, какой вариант вы поставите в отчёт и что напишете рядом.
По желанию.
- Замените обычный пробел на неразрывный (
\u00a0) и посмотрите в браузере, что изменилось при узком окне. - Посчитайте те же суммы в
Decimalи сравните итог сfloat. - Добавьте столбцу
font-variant-numeric: tabular-numsв CSS и посмотрите, как выровнялись цифры.
Куда это встанет в проекте
Пятнадцатый шаг: у сводки появляется sholu/pishim.py — единственное место, где число превращается в текст. Три функции: number — пробел и запятая по-нашему, signed — изменение со знаком (и округлённый ноль без плюса), percent — доля с той же запятой. Пропуск во всех трёх становится прочерком.
Страница берёт форматирование оттуда по имени столбца, и в ячейках вместо 8.690822 стоит 8,69. Заодно в стилях появилась строка font-variant-numeric: tabular-nums: без неё цифры разной ширины, и столбец не выравнивается, как его ни форматируй.
Долги. Единицы у сводки живут в заголовках столбцов — орташа, ең_жоғары, — и то, что это проценты, читатель узнаёт только из заголовка страницы. Когда в отчёт придут деньги, заголовки придётся переписать так, чтобы единица стояла у каждого столбца своя.
Ответы
Показать ответы
На вопросы
- Потому что Python округляет половинки к чётному:
0.5→0,1.5→2,2.5→2,3.5→4. Это не ошибка, а стандарт: при множестве округлений ошибка не накапливается в одну сторону. «Половина вверх» делается черезDecimalсROUND_HALF_UP. - Потому что каждая строка при округлении сдвигается в свою сторону, и эти сдвиги складываются. Четыре строки по 10.005 дают 40.04 округлёнными и 40.02 — округлённой суммой. Итог считают по неокруглённым, а расхождение либо показывают, либо дописывают в самую большую строку.
- В заголовке столбца. Тогда числа в ячейках читаются как числа и выравниваются по разряду; повторённая в каждой строке единица мешает и глазу, и сравнению.
К разминке
- Каждый формат делает ровно одно:
.0fубирает дробную часть,,расставляет разделители,+показывает знак,>12выравнивает по ширине, а деление на тысячу — это уже не формат, а другая единица.
45678
45,678.40
+45678.4
| 45,678.4|
45.7 тыс.
.replace(",", " ").replace(".", ",")— именно в таком порядке.
amount = 1234567.891
print(f"{amount:,.2f}".replace(",", " ").replace(".", ","))
1 234 567,89
:.1%умножает на сто сам. Умножать до него — значит сделать это дважды.
share = 0.1274
print(f"доля: {share:.1%}")
print(f"или так: {share * 100:.1f} %")
доля: 12.7%
или так: 12.7 %
К заданию
Форматирование вынесено в money и percent не ради красоты: одинаковая запятая в суммах и процентах — это то, что держит таблицу единой. Стоит поменять её в одном месте — и таблица начинает выглядеть как две.
Итог считается из исходных чисел, а расхождение с суммой округлённых печатается рядом. Это и есть честный ответ на «не сходится копейка»: она не сходится по устройству арифметики, и отчёт должен об этом говорить, а не прятать.
Источники
- Мини-язык форматирования — всё, что можно написать после двоеточия.
- Модуль decimal — деньги, правила округления и почему
floatдля них не годится. - Функция round — округление к чётному в документации языка.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.