Во-первых, функция для тех, кто просто хочет сома e код копирования и вставки:
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 или даже любой язык программирования без ленивой оценки.Функция усечения имеет доступ только к двоичному значению, хранящемуся в памяти компьютера, а не к строке, которую вы действительно ввели в исходный код.
Если вы декодировать последовательность битов обратно в десятичное число, снова используя 64-битный формат IEEE с плавающей точкой, вы получите
0.2999999999999999888977697537484345957637...
так наивная реализация могла бы придумать 0.2
хотя это, вероятно, не то, что вы хотите. Подробнее о ошибке с плавающей точкой, see the Python tutorial.
Очень редко работать со значением с плавающей точкой, близким к круглому числу, и все же является намеренно, не равным этому круглому числу. Поэтому при усечении, вероятно, имеет смысл выбрать «самое приятное» десятичное представление из всего, что может соответствовать значению в памяти. Python 2.7 и выше (но не 3.0) включает в себя sophisticated algorithm to do just that, с которым мы можем получить доступ через операцию форматирования строк по умолчанию.
'{}'.format(f)
Единственное ограничение в том, что это действует как спецификации формата g
, в том смысле, что он использует экспоненциальное обозначение (1.23e+4
), если число является большим или достаточно мало. Поэтому метод должен поймать этот случай и обрабатывать его по-разному. Есть несколько случаев, когда использование спецификации формата f
вызывает проблему, например, попытку обрезания 3e-10
до 28 цифр точности (она производит 0.0000000002999999999999999980
), и я еще не знаю, как лучше всего их обрабатывать.
Если вы на самом деле являетесь работой с float
с, которые очень близки к округлить, но намеренно не приравненные к ним (как 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
Если это ваша ситуация, если вы do хотите использовать «приятные» десятичные представления для усечения, все, что вы можете сделать (насколько мне известно), - выбрать некоторое количество цифр, меньше полной точности, представляемой float
, и округлить число до этого большого числа цифр перед усечением. Типичный выбор - 12,
'%.12f' % f
, но вы можете отрегулировать его в соответствии с числами, которые вы используете.
Ну ... Я соврал.Технически вы можете поручить 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
Обобщая это обрабатывает случай, когда вы передаете в переменном, кажется, как безнадежны, так как вы должны проследить назад через выполнение программы, пока вы не найдете: представлена только для развлечения следующего с плавающей точкой, который дал переменной значение. Если есть даже один. Большинство переменных будут инициализированы из пользовательского ввода или математических выражений, и в этом случае двоичное представление есть все, что есть.
Если -1.233 быть укорочены до -1.23 или -1.24? –
@Mad Physicist Вы имеете в виду, что округление -1.233 может оказаться равным -1.24? Я имею в виду усечение до минус бесконечности. –
OIC. Это было глупо. Удаление комментария. –