Почему не добавят во встроенный интерпретатор функцию типа round только для математического, а не банковского округления?
Трудно сказать, возможно потому, что распространённые процессоры (x86/ARM) поддерживают 4 типа округления плавающей точки, как и традиционные языки/ОС (Fortran-2003/C17/C++/POSIX):
- FE_DOWNWARD (roundTowardNegative или
floor()
);
- FE_TONEAREST (roundTiesToEven или
nearbyint()
, округление по-умолчанию, rint()
);
- FE_TOWARDZERO (roundTowardZero или
int()
);
- FE_UPWARD (roundTowardPositive или
ceil()
);
- И только Fortran-2018 и C23 описали: FE_TONEARESTFROMZERO (roundTiesToAway или
round()
).
Правда, это же не помешало же изначально иметь функцию round()
в C/C++/POSIX? Ну, да, не одна команда, как rint()
, но некоторым нравится, да и нужно ж иногда.
И есть ли варианты округлять проще с точки зрения производительности и
читабельности кода?
А какие у нас есть ещё варианты?
Стандартный, для float
(без поддержки math.inf
и math.nan
):
import math
def math_roundTiesToAway(x):
return math.trunc(x + math.copysign(0.5, x))
Для массивов и типов NumPy:
import numpy as np
def np_roundTiesToAway(x):
return np.trunc(x + np.copysign(0.5, x))
В смысле, читаемости (т.е. отсутствию необходимости соображать в математике):
import ctypes
import sys
if sys.platform.startswith("darwin"):
libc = ctypes.CDLL("libSystem.B.dylib")
elif sys.platform.startswith('win'):
libc = ctypes.cdll.msvcrt
else:
libc = ctypes.CDLL("libc.so.6")
libc.round.argtypes = [ctypes.c_double]
libc.round.restype = ctypes.c_double
roundTiesToAway = libc.round
Но он длинный и, для float
, не слишком шустрый. Зато, как и np_roundTiesToAway()
, с math.inf
и math.nan
дружит, всё согласно IEEE-754.
Есть варианты с Cython, они, и шустрее, и используют round()
из С (из libc), т.е. надёжнее.
x = 2.675
a0 = np.double(x)
a1000 = np.full(1000, a0)
q = Decimal("1.")
%timeit math_roundTiesToAway(x)
%timeit float(math_roundTiesToAway(x))
%timeit np_roundTiesToAway(a0)
%timeit np_roundTiesToAway(a1000)
%timeit roundTiesToAway(x)
%timeit Decimal(x).quantize(q, ROUND_HALF_UP)
99.6 ns ± 2.89 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
130 ns ± 11.7 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
1.73 μs ± 22.4 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
3.81 μs ± 96.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
348 ns ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
868 ns ± 9.77 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Поскольку math_roundTiesToAway(x)
(math.trunc(x)
) имеет тип int
, добавлена оценка для float(math_roundTiesToAway(x))
.
P.S.
Хотя с округлением к целому всё неплохо. Однако, что касается округления до N-цифр после запятой, то постановка задачи начинает выглядеть неоднозначной.
Если аргумент имеет тип float
, то, так же как и при использовании стандартной функции round(), можно получить немного неожиданные результаты, вызванные двоичным представлением аргумента с ограниченной точностью:
x |
x.hex() |
≈x |
ndigits |
decimal_roundTiesToAway |
math_roundTiesToAway |
0.15 |
0x1.3333333333333p-3 |
≈0.14999999999999999 |
1 |
0.1 |
0.2 |
0.145 |
0x1.28f5c28f5c28fp-3 |
≈0.14499999999999999 |
2 |
0.14 |
0.14 |
2.675 |
0x1.5666666666666p+1 |
≈2.6749999999999998 |
2 |
2.67 |
2.68 |
2.0115 |
0x1.0178d4fdf3b64p+1 |
≈2.0114999999999998 |
3 |
2.011 |
2.011 |
0.0000005 |
0x1.0c6f7a0b5ed8dp-21 |
≈4.9999999999999998e-7 |
6 |
0.000000 |
1e-06 |
Если цели применения roundTiesToAway()
сугубо вычислительные/математические, к примеру, равномерное распределение младших цифр, или уменьшение вероятности появления 0 вдвое, то это не составляет проблемы. Всё строго формально корректно и все цели достигнуты.
Поскольку для типа Decimal
умножение на степень 10 - точное, таких неожиданностей больше, почти все. Ошибки округления при операции math.fma(x, q, math.copysign(0.5, x))
немного уменьшают вероятность таких неожиданностей, но не исключают их полностью.
Однако, если цель получить красивый результат или удовлетворить каким-то иным требованиям, то постановку задачи можно видоизменить, т.е. округлять вверх если граница половины младшей цифры попадает в диапазон [x - 𝛿, x + 𝛿]
, где 𝛿 больше 0, но меньше math.ulp(x)
(т.е. если число i..i,f..f50..0
равняется x
с машинной точность, то следует применить правило десятичного округления).
import decimal
_drtta_q0 = decimal.Decimal("1")
def decimal_roundTiesToAway(x, ndigits=0, pretty=False,
pretty_delta_x = float.fromhex('0x1.8p-1') # 0.75ulp
):
"""
decimal_roundTiesToAway(x, n) - округлённое до n-цифр машинное представление x
str(decimal_roundTiesToAway(x, n, pretty=True)) - округлённое до n-цифр str(x)
"""
dx = decimal.Decimal(x)
if not ndigits:
return dx.quantize(_drtta_q0, decimal.ROUND_HALF_UP)
# assert math.ulp(x)/math.fabs(dx - dx.next_toward(0)) >= 2, \
# "Default context precision too small"
q = decimal.Decimal(f"1e{-ndigits}")
if not pretty or isinstance(x, str) or isinstance(x, decimal.Decimal):
return dx.quantize(q, decimal.ROUND_HALF_UP)
return (dx +
decimal.Decimal(math.copysign(pretty_delta_x*math.ulp(x), x))
).quantize(q, decimal.ROUND_HALF_UP)
import math
try:
from math import fma as lc_fma # Python 3.13 и выше
except ImportError:
import ctypes
import sys
if sys.platform.startswith("darwin"):
_libc = ctypes.CDLL("libSystem.B.dylib")
elif sys.platform.startswith('win'):
_libc = ctypes.cdll.msvcrt
else:
_libc = ctypes.CDLL("libc.so.6")
_libc.fma.argtypes = [ctypes.c_double, ctypes.c_double, ctypes.c_double]
_libc.fma.restype = ctypes.c_double
lc_fma = _libc.fma
def math_roundTiesToAway(x, ndigits=0, pretty=False,
pretty_delta_x = float.fromhex('0x1.p-1') # 0.5ulp
):
"""
math_roundTiesToAway(x, n) - округлённое до n-цифр машинное представление x
str(math_roundTiesToAway(x, n, pretty=True)) - округлённое до n-цифр str(x)
"""
if not ndigits:
return math.trunc(x + math.copysign(0.5, x))
if ndigits < 0:
q = float(10**-ndigits)
return math.trunc(x/q + math.copysign(0.5, x))*q
q = float(10**ndigits)
if not pretty:
return math.trunc(lc_fma(x, q, math.copysign(0.5, x)))/q
xl = math.copysign(pretty_delta_x*math.ulp(x), x)
h = x + xl
l = xl - (h - x)
x, xl = h, l
# assert x == x + xl and abs(xl) < math.ulp(x)
ql = float(10**ndigits - int(q))
# assert q == (q + ql) and abs(ql) < math.ulp(q)
h = x*q
l = lc_fma(x, q, -h)
l += ql*x + xl*q
xq = h + l
xql = l - (xq - h)
# assert xq == (xq + xql) and abs(xql) < math.ulp(xq)
a05 = math.copysign(0.5, x)
h = xq + a05
t = h - xq
l = (xq - (h - t)) + (a05 - t)
l += xql
xqa05 = h + l
xqa05l = l - (xqa05 - h)
# assert xqa05 == (xqa05 + xqa05l) and abs(xqa05l) < math.ulp(xqa05)
txqa05 = math.trunc(xqa05)
if txqa05 == xqa05:
txqa05 += math.floor(xqa05l) if 0 < xqa05 else math.ceil(xqa05l)
return txqa05/q
print(f"{decimal_roundTiesToAway(2.0115, 3)=}")
%timeit decimal_roundTiesToAway(2.0115, 3)
print(f"{math_roundTiesToAway(2.0115, 3)=}")
%timeit math_roundTiesToAway(2.0115, 3)
decimal_roundTiesToAway(2.0115, 3)=Decimal('2.011')
1.47 μs ± 15.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
math_roundTiesToAway(2.0115, 3)=2.011
267 ns ± 2.42 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
print(f"{decimal_roundTiesToAway(2.0115, 3, pretty=True)=}")
%timeit decimal_roundTiesToAway(2.0115, 3, pretty=True)
print(f"{math_roundTiesToAway(2.0115, 3, pretty=True)=}")
%timeit math_roundTiesToAway(2.0115, 3, pretty=True)
decimal_roundTiesToAway(2.0115, 3, pretty=True)=Decimal('2.012')
2.42 μs ± 74.8 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
math_roundTiesToAway(2.0115, 3, pretty=True)=2.012
835 ns ± 13 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)