Innanzitutto, la funzione, per coloro che desiderano solo un codice copia e incolla:
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]])
Questo è valido in Python 2.7 e 3.1+. Per le versioni precedenti, non è possibile ottenere lo stesso effetto di "arrotondamento intelligente" (almeno, non senza molto codice complicato), ma l'arrotondamento a 12 cifre decimali prima del troncamento funzionerà per la maggior parte del 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]])
Spiegazione
Il nucleo del metodo sottostante è convertire il valore in una stringa con la massima precisione e quindi tagliare tutto oltre il numero di caratteri desiderato. L'ultimo passaggio è facile; può essere fatto sia con la manipolazione delle stringhe
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
o il decimal
modulo
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
Il primo passo, la conversione in una stringa, è abbastanza difficile perché ci sono alcune coppie di letterali in virgola mobile (cioè ciò che scrivi nel codice sorgente) che producono entrambi la stessa rappresentazione binaria e tuttavia dovrebbero essere troncati in modo diverso. Ad esempio, considera 0,3 e 0,29999999999999998. Se scrivi 0.3
in un programma Python, il compilatore lo codifica utilizzando il formato a virgola mobile IEEE nella sequenza di bit (assumendo un float a 64 bit)
0011111111010011001100110011001100110011001100110011001100110011
Questo è il valore più vicino a 0,3 che può essere rappresentato con precisione come un float IEEE. Ma se scrivi 0.29999999999999998
in un programma Python, il compilatore lo traduce esattamente nello stesso valore . In un caso, volevi che fosse troncato (a una cifra) come 0.3
, mentre nell'altro caso lo volevi troncato come 0.2
, ma Python può dare solo una risposta. Questa è una limitazione fondamentale di Python, o addirittura di qualsiasi linguaggio di programmazione senza una valutazione pigra. La funzione di troncamento ha accesso solo al valore binario archiviato nella memoria del computer, non alla stringa che hai effettivamente digitato nel codice sorgente. 1
Se decodifichi la sequenza di bit in un numero decimale, sempre utilizzando il formato a virgola mobile IEEE a 64 bit, ottieni
0.2999999999999999888977697537484345957637...
quindi un'implementazione ingenua verrebbe fuori 0.2
anche se probabilmente non è quello che vuoi. Per ulteriori informazioni sull'errore di rappresentazione in virgola mobile, vedere il tutorial di Python .
È molto raro lavorare con un valore a virgola mobile che è così vicino a un numero tondo e tuttavia non è intenzionalmente uguale a quel numero tondo. Quindi, durante il troncamento, probabilmente ha senso scegliere la rappresentazione decimale "più gradevole" tra tutte quelle che potrebbero corrispondere al valore in memoria. Python 2.7 e versioni successive (ma non 3.0) include un sofisticato algoritmo per fare proprio questo , a cui possiamo accedere tramite l'operazione di formattazione delle stringhe predefinita.
'{}'.format(f)
L'unico avvertimento è che questo agisce come una g
specifica di formato, nel senso che usa la notazione esponenziale ( 1.23e+4
) se il numero è abbastanza grande o abbastanza piccolo. Quindi il metodo deve catturare questo caso e gestirlo in modo diverso. Ci sono alcuni casi in cui l'utilizzo di una f
specifica di formato causa invece un problema, come il tentativo di troncare 3e-10
a 28 cifre di precisione (produce 0.0000000002999999999999999980
), e non sono ancora sicuro di come gestirle al meglio.
Se effettivamente sta lavorando con float
s che sono molto vicino a cifra tonda, ma volutamente non uguale a loro (come 0,29999999999999998 o 99,959999999999994), questo produrrà alcuni falsi positivi, vale a dire che sarà cifra tonda che non volevano arrotondata. In tal caso la soluzione è specificare una precisione fissa.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Il numero di cifre di precisione da usare qui non ha molta importanza, deve solo essere abbastanza grande da garantire che qualsiasi arrotondamento eseguito nella conversione di stringa non "aumenti" il valore alla sua bella rappresentazione decimale. Penso che sys.float_info.dig + n + 2
possa essere sufficiente in tutti i casi, ma in caso contrario 2
potrebbe essere necessario aumentare, e non fa male farlo.
Nelle versioni precedenti di Python (fino a 2.6 o 3.0), la formattazione del numero in virgola mobile era molto più rozza e produceva regolarmente cose come
>>> 1.1
1.1000000000000001
Se questa è la tua situazione, se non vuole usare "belle" rappresentazioni decimali per troncamento, tutto si può fare (per quanto ne so) è prendere un numero di cifre, meno del rappresentabile massima precisione da una float
, e intorno al numero a quel numero di cifre prima di troncarlo. Una scelta tipica è 12,
'%.12f' % f
ma puoi modificarlo per adattarlo ai numeri che stai utilizzando.
1 Beh ... ho mentito. Tecnicamente, puoi istruire Python a rianalizzare il proprio codice sorgente ed estrarre la parte corrispondente al primo argomento passato alla funzione di troncamento. Se quell'argomento è un letterale a virgola mobile, puoi semplicemente tagliarlo fuori un certo numero di posizioni dopo il punto decimale e restituirlo. Tuttavia questa strategia non funziona se l'argomento è una variabile, il che lo rende abbastanza inutile. Quanto segue è presentato solo a scopo di intrattenimento:
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
Generalizzare questo per gestire il caso in cui passi una variabile sembra una causa persa, dal momento che dovresti risalire all'indietro attraverso l'esecuzione del programma fino a trovare il valore letterale a virgola mobile che ha dato alla variabile il suo valore. Se ce n'è anche uno. La maggior parte delle variabili verrà inizializzata dall'input dell'utente o da espressioni matematiche, nel qual caso la rappresentazione binaria è tutto ciò che c'è.