Primeiro, a função, para aqueles que querem apenas um código de copiar e colar:
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]])
Isso é válido no Python 2.7 e 3.1+. Para versões mais antigas, não é possível obter o mesmo efeito de "arredondamento inteligente" (pelo menos, não sem muitos códigos complicados), mas arredondar para 12 casas decimais antes do truncamento funcionará na maior parte do tempo:
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]])
Explicação
O núcleo do método subjacente é converter o valor em uma string com precisão total e, em seguida, cortar tudo além do número desejado de caracteres. A última etapa é fácil; pode ser feito com manipulação de string
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
ou o decimal
módulo
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
A primeira etapa, a conversão para uma string, é bastante difícil porque existem alguns pares de literais de ponto flutuante (ou seja, o que você escreve no código-fonte) que produzem a mesma representação binária e, ainda assim, devem ser truncados de forma diferente. Por exemplo, considere 0,3 e 0,29999999999999998. Se você escrever 0.3
em um programa Python, o compilador o codifica usando o formato de ponto flutuante IEEE na sequência de bits (assumindo um float de 64 bits)
0011111111010011001100110011001100110011001100110011001100110011
Este é o valor mais próximo de 0,3 que pode ser representado com precisão como um flutuador IEEE. Mas se você escrever 0.29999999999999998
em um programa Python, o compilador o traduzirá exatamente no mesmo valor . Em um caso, você pretendia que fosse truncado (para um dígito) como 0.3
, enquanto no outro caso você pretendia que fosse truncado como 0.2
, mas Python pode dar apenas uma resposta. Esta é uma limitação fundamental do Python, ou mesmo de qualquer linguagem de programação sem avaliação preguiçosa. A função de truncamento só tem acesso ao valor binário armazenado na memória do computador, não à string que você realmente digitou no código-fonte. 1
Se você decodificar a sequência de bits de volta para um número decimal, novamente usando o formato de ponto flutuante IEEE de 64 bits, você obtém
0.2999999999999999888977697537484345957637...
portanto, uma implementação ingênua surgiria, 0.2
mesmo que provavelmente não seja o que você deseja. Para obter mais informações sobre o erro de representação de ponto flutuante, consulte o tutorial Python .
É muito raro trabalhar com um valor de ponto flutuante tão próximo a um número redondo e, ainda assim, intencionalmente não igual a esse número redondo. Portanto, ao truncar, provavelmente faz sentido escolher a representação decimal "mais adequada" de todas as que podem corresponder ao valor na memória. O Python 2.7 e superior (mas não 3.0) inclui um algoritmo sofisticado para fazer exatamente isso , que podemos acessar por meio da operação de formatação de string padrão.
'{}'.format(f)
A única ressalva é que isso funciona como uma g
especificação de formato, no sentido de que usa notação exponencial ( 1.23e+4
) se o número for grande ou pequeno o suficiente. Portanto, o método deve capturar esse caso e tratá-lo de maneira diferente. Existem alguns casos em que o uso de uma f
especificação de formato causa um problema, como tentar truncar 3e-10
para 28 dígitos de precisão (produz 0.0000000002999999999999999980
), e ainda não tenho certeza da melhor forma de lidar com isso.
Se você realmente estiver trabalhando com float
s que são muito próximos de números redondos, mas intencionalmente não iguais a eles (como 0,29999999999999998 ou 99,959999999999994), isso produzirá alguns falsos positivos, ou seja, arredondará números que você não deseja arredondar. Nesse caso, a solução é especificar uma precisão fixa.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
O número de dígitos de precisão a ser usado aqui não importa realmente, ele só precisa ser grande o suficiente para garantir que qualquer arredondamento executado na conversão de string não "aumente" o valor para sua bela representação decimal. Acho que sys.float_info.dig + n + 2
pode ser suficiente em todos os casos, mas, se não, 2
talvez seja necessário aumentar, e não faz mal fazê-lo.
Em versões anteriores do Python (até 2.6 ou 3.0), a formatação do número de ponto flutuante era muito mais rudimentar e costumava produzir coisas como
>>> 1.1
1.1000000000000001
Se esta é sua situação, se você não quiser usar "nice" representações decimais para truncagem, tudo o que você pode fazer (tanto quanto eu sei) é escolher um número de dígitos, menos do que o representável precisão total por um float
, e volta a número com essa quantidade de dígitos antes de truncá-lo. Uma escolha típica é 12,
'%.12f' % f
mas você pode ajustar isso para se adequar aos números que está usando.
1 Bem ... eu menti. Tecnicamente, você pode instruir o Python a analisar novamente seu próprio código-fonte e extrair a parte correspondente ao primeiro argumento que você passa para a função de truncamento. Se esse argumento for um literal de ponto flutuante, você pode simplesmente cortá-lo com um certo número de casas após o ponto decimal e retornar isso. No entanto, essa estratégia não funciona se o argumento for uma variável, o que o torna bastante inútil. O seguinte é apresentado apenas para valor de entretenimento:
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
Generalizar isso para lidar com o caso em que você passa uma variável parece uma causa perdida, já que você teria que rastrear a execução do programa até encontrar o literal de ponto flutuante que deu à variável seu valor. Se houver um. A maioria das variáveis será inicializada a partir da entrada do usuário ou de expressões matemáticas, caso em que a representação binária é tudo o que existe.