Erstens die Funktion für diejenigen, die nur Code zum Kopieren und Einfügen benötigen:
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]])
Dies gilt in Python 2.7 und 3.1+. Bei älteren Versionen ist es nicht möglich, den gleichen "intelligenten Rundungseffekt" zu erzielen (zumindest nicht ohne viel komplizierten Code), aber das Runden auf 12 Dezimalstellen vor dem Abschneiden funktioniert meistens:
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]])
Erläuterung
Der Kern der zugrunde liegenden Methode besteht darin, den Wert mit voller Genauigkeit in eine Zeichenfolge umzuwandeln und dann einfach alles über die gewünschte Anzahl von Zeichen hinaus abzuschneiden. Der letzte Schritt ist einfach; Dies kann entweder mit String-Manipulation erfolgen
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
oder das decimal
Modul
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
Der erste Schritt, die Konvertierung in eine Zeichenfolge, ist ziemlich schwierig, da es einige Paare von Gleitkomma-Literalen gibt (dh was Sie im Quellcode schreiben), die beide dieselbe Binärdarstellung erzeugen und dennoch unterschiedlich abgeschnitten werden sollten. Betrachten Sie beispielsweise 0,3 und 0,29999999999999998. Wenn Sie 0.3
in ein Python-Programm schreiben , codiert der Compiler es im IEEE-Gleitkommaformat in die Bitfolge (unter der Annahme eines 64-Bit-Gleitkommas).
0011111111010011001100110011001100110011001100110011001100110011
Dies ist der Wert, der 0,3 am nächsten kommt und genau als IEEE-Float dargestellt werden kann. Wenn Sie jedoch 0.29999999999999998
in ein Python-Programm schreiben , übersetzt der Compiler es in genau denselben Wert . In einem Fall haben Sie gemeint, dass es abgeschnitten wird (auf eine Ziffer) 0.3
, während Sie in dem anderen Fall gemeint haben, dass es abgeschnitten wird 0.2
, aber Python kann nur eine Antwort geben. Dies ist eine grundlegende Einschränkung von Python oder einer anderen Programmiersprache ohne verzögerte Auswertung. Die Kürzungsfunktion hat nur Zugriff auf den im Speicher des Computers gespeicherten Binärwert, nicht auf die Zeichenfolge, die Sie tatsächlich in den Quellcode eingegeben haben. 1
Wenn Sie die Bitfolge wieder in eine Dezimalzahl dekodieren und erneut das IEEE 64-Bit-Gleitkommaformat verwenden, erhalten Sie
0.2999999999999999888977697537484345957637...
Eine naive Implementierung würde sich also einfallen lassen, 0.2
obwohl Sie das wahrscheinlich nicht wollen. Weitere Informationen zum Gleitkomma-Darstellungsfehler finden Sie im Python-Lernprogramm .
Es ist sehr selten, mit einem Gleitkommawert zu arbeiten, der einer runden Zahl so nahe kommt und dennoch absichtlich nicht dieser runden Zahl entspricht. Wenn Sie also abschneiden, ist es wahrscheinlich sinnvoll, die "schönste" Dezimaldarstellung aus allen auszuwählen, die dem Wert im Speicher entsprechen könnten. Python 2.7 und höher (aber nicht 3.0) enthält einen ausgeklügelten Algorithmus , auf den wir über die Standardformatierungsoperation für Zeichenfolgen zugreifen können.
'{}'.format(f)
Die einzige Einschränkung besteht darin, dass dies wie eine Formatspezifikation wirkt g
, in dem Sinne, dass die Exponentialnotation ( 1.23e+4
) verwendet wird, wenn die Zahl groß oder klein genug ist. Die Methode muss diesen Fall also erfassen und anders behandeln. Es gibt einige Fälle, in denen die Verwendung einer f
Formatspezifikation stattdessen ein Problem verursacht, z. B. der Versuch, die 3e-10
Genauigkeit auf 28 Stellen abzuschneiden (dies führt zu 0.0000000002999999999999999980
), und ich bin mir noch nicht sicher, wie ich mit diesen am besten umgehen soll.
Wenn Sie tatsächlich sind mit Arbeits float
s , die sehr nah an runden Zahlen sind aber absichtlich gleich sie nicht (wie ,29999999999999998 oder 99,959999999999994), wird dies einige Fehlalarme produzieren, das heißt , es werden rund um Zahlen , dass Sie nicht gerundet haben wollten. In diesem Fall besteht die Lösung darin, eine feste Genauigkeit anzugeben.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Die Anzahl der hier zu verwendenden Genauigkeitsziffern spielt keine Rolle, sie muss nur groß genug sein, um sicherzustellen, dass bei der bei der Zeichenfolgenkonvertierung durchgeführten Rundung der Wert nicht auf seine schöne Dezimaldarstellung "erhöht" wird. Ich denke, es sys.float_info.dig + n + 2
kann in allen Fällen ausreichen, aber wenn nicht, muss das 2
möglicherweise erhöht werden, und es tut nicht weh, dies zu tun.
In früheren Versionen von Python (bis zu 2.6 oder 3.0) war die Formatierung von Gleitkommazahlen viel gröber und erzeugte regelmäßig Dinge wie
>>> 1.1
1.1000000000000001
Wenn dies Ihre Situation ist und Sie "schöne" Dezimaldarstellungen zum Abschneiden verwenden möchten, können Sie (soweit ich weiß) nur eine Anzahl von Ziffern auswählen, die unter der durch a darstellbaren vollen Genauigkeit liegen float
, und die Zahl abrunden Nummerieren Sie diese Anzahl, bevor Sie sie abschneiden. Eine typische Wahl ist 12,
'%.12f' % f
Sie können dies jedoch an die von Ihnen verwendeten Zahlen anpassen.
1 Nun ... ich habe gelogen. Technisch Sie können Python anweisen , seinen eigenen Quellcode neu zu analysieren und den Teil , den Sie zum Abschneiden Funktion übergeben , um das erste Argument entspricht , zu extrahieren. Wenn dieses Argument ein Gleitkomma-Literal ist, können Sie es einfach um eine bestimmte Anzahl von Stellen nach dem Dezimalpunkt abschneiden und dieses zurückgeben. Diese Strategie funktioniert jedoch nicht, wenn das Argument eine Variable ist, was es ziemlich nutzlos macht. Folgendes wird nur zum Unterhaltungswert dargestellt:
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
Dies zu verallgemeinern, um den Fall zu behandeln, in dem Sie eine Variable übergeben, scheint eine verlorene Ursache zu sein, da Sie die Programmausführung rückwärts verfolgen müssten, bis Sie das Gleitkomma-Literal finden, das der Variablen ihren Wert gegeben hat. Wenn es überhaupt einen gibt. Die meisten Variablen werden aus Benutzereingaben oder mathematischen Ausdrücken initialisiert. In diesem Fall ist nur die binäre Darstellung vorhanden.