Во-первых, функция для тех, кто просто хочет скопировать и вставить код:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '{}'.format(f)
if 'e' in s or 'E' in s:
return '{0:.{1}f}'.format(f, n)
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Это действительно в Python 2.7 и 3.1+. Для более старых версий невозможно получить тот же эффект «интеллектуального округления» (по крайней мере, не без большого количества сложного кода), но округление до 12 десятичных знаков перед усечением будет работать большую часть времени:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '%.12f' % f
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
объяснение
Суть базового метода состоит в том, чтобы преобразовать значение в строку с полной точностью, а затем просто отрубить все, что превышает желаемое количество символов. Последний шаг прост; это можно сделать либо с помощью строковых манипуляций
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
или decimal
модуль
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
Первый шаг, преобразование в строку, довольно сложен, потому что есть несколько пар литералов с плавающей запятой (то есть то, что вы пишете в исходном коде), которые оба производят одно и то же двоичное представление, но должны быть по-разному усечены. Например, рассмотрим 0,3 и 0,29999999999999998. Если вы пишете 0.3
программу Python, компилятор кодирует ее, используя формат с плавающей запятой IEEE, в последовательность битов (при условии 64-битного числа с плавающей запятой)
0011111111010011001100110011001100110011001100110011001100110011
Это ближайшее значение к 0,3, которое может быть точно представлено как число с плавающей запятой IEEE. Но если вы пишете 0.29999999999999998
программу Python, компилятор переводит ее в точно такое же значение . В одном случае вы имели в виду его усечение (до одной цифры) как 0.3
, тогда как в другом случае вы имели в виду его усечение как 0.2
, но Python может дать только один ответ. Это фундаментальное ограничение Python или любого другого языка программирования без ленивых вычислений. Функция усечения имеет доступ только к двоичному значению, хранящемуся в памяти компьютера, а не к строке, которую вы фактически ввели в исходный код. 1
Если вы декодируете последовательность битов обратно в десятичное число, снова используя 64-битный формат с плавающей запятой IEEE, вы получите
0.2999999999999999888977697537484345957637...
поэтому возникнет наивная реализация, 0.2
даже если это, вероятно, не то, что вы хотите. Подробнее об ошибке представления с плавающей запятой см. В учебнике Python .
Очень редко приходится работать со значением с плавающей запятой, которое так близко к круглому числу, но намеренно не равно этому круглому числу. Таким образом, при усечении, вероятно, имеет смысл выбрать «самое красивое» десятичное представление из всего, что может соответствовать значению в памяти. Python 2.7 и выше (но не 3.0) включает в себя сложный алгоритм для этого , доступ к которому мы можем получить с помощью операции форматирования строк по умолчанию.
'{}'.format(f)
Единственное предостережение: это действует как g
спецификация формата в том смысле, что в нем используется экспоненциальная запись ( 1.23e+4
), если число велико или достаточно мало. Таким образом, метод должен уловить этот случай и обработать его по-другому. Есть несколько случаев, когда использование f
спецификации формата вместо этого вызывает проблему, например, попытка усечения 3e-10
до 28 цифр точности (это дает 0.0000000002999999999999999980
), и я еще не уверен, как лучше всего с этим справиться.
Если вы на самом деле являетесь работой с float
S, которые очень близка к округлить , но намеренно не приравненную к ним (как 0.29999999999999998 или 99.959999999999994), это будет производить некоторые ложные срабатывания, то есть это будут круглые цифры , которые вы не хотите округленной. В этом случае решение состоит в том, чтобы указать фиксированную точность.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Используемое здесь количество цифр точности не имеет значения, оно должно быть достаточно большим, чтобы гарантировать, что любое округление, выполняемое при преобразовании строки, не «увеличивает» значение до его красивого десятичного представления. Думаю, sys.float_info.dig + n + 2
может быть достаточно во всех случаях, но если нет, то, 2
возможно, придется увеличить, и это не повредит.
В более ранних версиях Python (до 2.6 или 3.0) форматирование чисел с плавающей запятой было намного более грубым и регулярно приводило к таким вещам, как
>>> 1.1
1.1000000000000001
Если это ваша ситуация, если вы действительно хотите использовать "хорошие" десятичные представления для усечения, все, что вы можете сделать (насколько мне известно), - это выбрать некоторое количество цифр, меньшее, чем полная точность, представленная a float
, и округлить число до этого количества цифр перед его усечением. Типичный выбор - 12,
'%.12f' % f
но вы можете настроить это в соответствии с используемыми числами.
+1 Ну ... соврал. Технически вы можете указать Python повторно проанализировать собственный исходный код и извлечь часть, соответствующую первому аргументу, который вы передаете функции усечения. Если этот аргумент является литералом с плавающей запятой, вы можете просто отрезать его через определенное количество знаков после десятичной точки и вернуть его. Однако эта стратегия не работает, если аргумент является переменной, что делает ее бесполезной. Следующее представлено только для развлечения:
def trunc_introspect(f, n):
'''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
current_frame = None
caller_frame = None
s = inspect.stack()
try:
current_frame = s[0]
caller_frame = s[1]
gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
for token_type, token_string, _, _, _ in gen:
if token_type == tokenize.NAME and token_string == current_frame[3]:
next(gen) # left parenthesis
token_type, token_string, _, _, _ = next(gen) # float literal
if token_type == tokenize.NUMBER:
try:
cut_point = token_string.index('.') + n + 1
except ValueError: # no decimal in string
return token_string + '.' + '0' * n
else:
if len(token_string) < cut_point:
token_string += '0' * (cut_point - len(token_string))
return token_string[:cut_point]
else:
raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
break
finally:
del s, current_frame, caller_frame
Обобщение этого для обработки случая, когда вы передаете переменную, кажется безнадежным делом, поскольку вам придется отслеживать выполнение программы в обратном направлении, пока вы не найдете литерал с плавающей запятой, который дал переменной ее значение. Если он вообще есть. Большинство переменных будут инициализированы из пользовательского ввода или математических выражений, и в этом случае двоичное представление - это все, что есть.