Python 3.x rundet die Hälfte ab

8

Ich weiß, dass Fragen zum Runden in Python bereits mehrmals gestellt wurden, aber die Antworten haben mir nicht geholfen. Ich suche nach einer Methode, die eine Float-Nummer halbiert und eine Float-Nummer zurückgibt. Die Methode sollte auch einen Parameter akzeptieren, der die Dezimalstelle definiert, auf die gerundet werden soll. Ich habe eine Methode geschrieben, die diese Art der Rundung implementiert. Ich denke jedoch, dass es überhaupt nicht elegant aussieht.

def round_half_up(number, dec_places):
    s = str(number)

    d = decimal.Decimal(s).quantize(
        decimal.Decimal(10) ** -dec_places,
        rounding=decimal.ROUND_HALF_UP)

    return float(d)

Ich mag es nicht, dass ich float in einen String konvertieren muss (um Gleitkomma-Ungenauigkeiten zu vermeiden) und dann mit dem Dezimalmodul arbeiten muss . Haben Sie bessere Lösungen?

Bearbeiten: Wie in den Antworten unten ausgeführt, ist die Lösung meines Problems nicht so offensichtlich, da eine korrekte Rundung in erster Linie die korrekte Darstellung von Zahlen erfordert und dies bei float nicht der Fall ist. Also ich würde erwarten, dass der folgende Code

def round_half_up(number, dec_places):

    d = decimal.Decimal(number).quantize(
        decimal.Decimal(10) ** -dec_places,
        rounding=decimal.ROUND_HALF_UP)

    return float(d)

(das unterscheidet sich vom obigen Code nur dadurch, dass die Gleitkommazahl direkt in eine Dezimalzahl und nicht zuerst in eine Zeichenfolge konvertiert wird), um 2.18 zurückzugeben, wenn es so verwendet wird: round_half_up(2.175, 2)Aber es wird nicht Decimal(2.175)zurückgegeben Decimal('2.17499999999999982236431605997495353221893310546875'), wie der Gleitkommawert Nummer wird vom Computer dargestellt. Überraschenderweise gibt der erste Code 2.18 zurück, da die Gleitkommazahl zuerst in einen String konvertiert wird. Es scheint, dass die Funktion str () eine implizite Rundung auf die Zahl durchführt, die ursprünglich gerundet werden sollte. Es finden also zwei Rundungen statt. Obwohl dies das Ergebnis ist, das ich erwarten würde, ist es technisch falsch.

IcesHay
quelle
@IcesHay Ich bin mir nicht sicher, ob ich verstehe, was du meinst, indem ich die Zahl halbiere. Können Sie uns einige Beispiele geben?
Kenivia
@ Kenivia Damit meine ich, dass Sie die relevante Dezimalstelle von 0-4 nach unten und von 5-9 nach oben runden: Wenn auf 0 Dezimalstellen gerundet wird: 2.4 = 2; 2,5 = 3; 3,5 = 4 usw.
IcesHay

Antworten:

7

Das Runden ist überraschend schwer richtig zu machen , da Sie Gleitkommaberechnungen sehr sorgfältig behandeln müssen. Wenn Sie nach einer eleganten Lösung suchen (kurz, leicht verständlich), ist das, was Sie mögen, ein guter Ausgangspunkt. Um korrekt zu sein, sollten Sie decimal.Decimal(str(number))die Dezimalstelle aus der Zahl selbst erstellen, um eine Dezimalversion der genauen Darstellung zu erhalten:

d = Decimal(number).quantize(...)...

Decimal(str(number))rundet effektiv zweimal , da das Formatieren des Gleitkommas in die Zeichenfolgendarstellung eine eigene Rundung durchführt. Dies liegt daran, str(float value)dass nicht versucht wird, die vollständige Dezimaldarstellung des Gleitkommas zu drucken, sondern nur dann genügend Ziffern gedruckt werden, um sicherzustellen, dass Sie denselben Gleitkommawert zurückerhalten, wenn Sie diese genauen Ziffern an den floatKonstruktor übergeben.

Wenn Sie richtige Rundung behalten wollen, aber vermeiden Sie auf der großen und komplexen je decimalModul können Sie sicher es tun, aber Sie müssen noch einige Weg , um die genaue arithmetics für die korrekte Rundung erforderlich umzusetzen. Beispielsweise können Sie Brüche verwenden :

import fractions, math

def round_half_up(number, dec_places=0):
    sign = math.copysign(1, number)
    number_exact = abs(fractions.Fraction(number))
    shifted = number_exact * 10**dec_places
    shifted_trunc = int(shifted)
    if shifted - shifted_trunc >= fractions.Fraction(1, 2):
        result = (shifted_trunc + 1) / 10**dec_places
    else:
        result = shifted_trunc / 10**dec_places
    return sign * float(result)

assert round_half_up(1.49) == 1
assert round_half_up(1.5) == 2
assert round_half_up(1.51) == 2
assert round_half_up(2.49) == 2
assert round_half_up(2.5) == 3
assert round_half_up(2.51) == 3

Beachten Sie, dass der einzige schwierige Teil im obigen Code die genaue Konvertierung eines Gleitkommas in einen Bruch ist und auf die Gleitkommamethode as_integer_ratio()entladen werden kann, was sowohl Dezimalstellen als auch Brüche intern tun. Wenn Sie also die Abhängigkeit von wirklich entfernen möchten fractions, können Sie die Brucharithmetik auf eine reine Ganzzahlarithmetik reduzieren. Sie bleiben auf Kosten der Lesbarkeit innerhalb der gleichen Zeilenanzahl:

def round_half_up(number, dec_places=0):
    sign = math.copysign(1, number)
    exact = abs(number).as_integer_ratio()
    shifted = (exact[0] * 10**dec_places), exact[1]
    shifted_trunc = shifted[0] // shifted[1]
    difference = (shifted[0] - shifted_trunc * shifted[1]), shifted[1]
    if difference[0] * 2 >= difference[1]:  # difference >= 1/2
        shifted_trunc += 1
    return sign * (shifted_trunc / 10**dec_places)

Beachten Sie, dass durch das Testen dieser Funktionen die beim Erstellen von Gleitkommazahlen durchgeführten Annäherungen hervorgehoben werden. Wird beispielsweise print(round_half_up(2.175, 2))gedruckt, 2.17weil die Dezimalzahl 2.175nicht exakt binär dargestellt werden kann. Sie wird daher durch eine Näherung ersetzt, die etwas kleiner als die Dezimalzahl von 2,175 ist. Die Funktion empfängt diesen Wert, findet ihn kleiner als den tatsächlichen Bruch, der der 2.175-Dezimalstelle entspricht, und beschließt, ihn abzurunden . Dies ist keine Eigenart der Implementierung. Das Verhalten leitet sich aus den Eigenschaften von Gleitkommazahlen ab und ist auch roundin Python 3 und 2 integriert .

user4815162342
quelle
Das Problem dabei d = Decimal(number).quantize(...)ist, dass es keine exakte Darstellung von Float-Zahlen gibt. So Decimal(2.175)geben Sie Dezimal ( ‚2,17499999 ...‘) (und somit wird eine falsche Rundung) , während Decimal('2.175')ist Dezimal ( ‚2.175‘). Deshalb konvertiere ich zuerst in einen String.
IcesHay
@IcesHay Es ist wahr, dass es keine exakte Darstellung einer Zahl wie 0.1 (dh des Bruchteils 1/10) in Binärform gibt, da ihr Nenner keine Potenz von 2 ist. Sobald Sie jedoch einen tatsächlichen Python-Float haben, ist die Annäherung bereits erfolgt und die Zahl , die Sie im Speicher haben tut eine genaue Darstellung hat, die Leistung erbracht wird as_integer_ratio()(im Fall von 0,1, das wäre 3602879701896397/36028797018963968 sein). Der decimal.Decimal(float)Konstruktor verwendet diese Darstellung und die nachfolgende Rundung ist korrekt und wird genau ausgeführt.
user4815162342
Bist du dir da sicher? Weil ich die Funktion getestet habe round_half_up(2.175, 2)und sie 2.17 zurückgegeben hat, was falsch ist. Vielleicht mache ich noch etwas falsch?
IcesHay
@IcesHay Das ist genau das Richtige. 2.175 wird beim Lesen als Float in eine Zahl angenähert, die genau als Bruchteil 2448832297382707/1125899906842624 dargestellt werden kann, der etwas kleiner als 2.175 ist. (Sie können dies testen, mit '%.20f' % 2.175dem ausgewertet wird '2.17499999999999982236'.) In Python 2.7, das eine halbe Aufrundung verwendet, gibt round (2.175) 2.17 zurück, und diese Art von Ergebnis wird als Einschränkung des Gleitkommas dokumentiert .
user4815162342
1
@IcesHay So funktioniert Gleitkomma nicht. Sobald eine Zeichenfolge in Gleitkomma eingelesen wurde, geht die ursprüngliche Zahl (die "2.175" von "2.174999999999 ..." unterscheiden würde) unwiederbringlich verloren. Bei Ihrer Frage geht es um die Implementierung der Aufrundung von Zahlen, die diese Antwort liefert. Es scheint, dass Ihr eigentliches Problem etwas ganz anderes ist, denn es stellt sich jetzt heraus, dass selbst Python 2 round(das Round-Half-Up implementiert) nicht gut genug ist. Bitte bearbeiten Sie die Frage, um den tatsächlichen Anwendungsfall anzugeben. Möglicherweise wird dies durch die Vermeidung von Gleitkommazahlen und die Verwendung von Dezimalstellen erfüllt.
user4815162342
1

Ich mag es nicht, dass ich float in einen String konvertieren muss (um Gleitkomma-Ungenauigkeiten zu vermeiden) und dann mit dem Dezimalmodul arbeiten muss. Haben Sie bessere Lösungen?

Ja; Verwenden DecimalSie diese Option, um Ihre Zahlen im gesamten Programm darzustellen, wenn Sie Zahlen wie 2.675 genau darstellen und auf 2.68 statt 2.67 runden müssen.

Es geht nicht anders. Die Gleitkommazahl, die auf Ihrem Bildschirm als 2.675 angezeigt wird, ist nicht die reelle Zahl 2.675. in der Tat ist es etwas weniger als 2,675, weshalb es auf 2,67 abgerundet wird:

>>> 2.675 - 2
0.6749999999999998

Es wird nur in Zeichenfolgenform angezeigt, '2.675'da dies zufällig die kürzeste Zeichenfolge ist float(s) == 2.6749999999999998. Beachten Sie, dass diese längere Darstellung (mit vielen 9s) auch nicht genau ist.

Wie auch immer Sie Ihre Rundungsfunktion schreiben, es ist nicht möglich my_round(2.675, 2), auf 2.68und auch my_round(2 + 0.6749999999999998, 2)auf aufzurunden 2.67; weil die Eingänge tatsächlich die gleiche Gleitkommazahl haben.

Wenn Ihre Nummer 2.675 jemals in einen Float und wieder zurück umgewandelt wird, haben Sie bereits die Information verloren, ob sie auf- oder abgerundet werden soll. Die Lösung besteht nicht darin, es überhaupt schweben zu lassen.

kaya3
quelle
-2

Nachdem ich sehr lange versucht hatte, eine elegante einzeilige Funktion zu erstellen, bekam ich etwas, das in seiner Größe mit einem Wörterbuch vergleichbar ist.

Ich würde sagen, der einfachste Weg, dies zu tun, ist nur zu

def round_half_up(inp,dec_places):
    return round(inp+0.0000001,dec_places)

Ich würde anerkennen, dass dies nicht in allen Fällen korrekt ist, aber funktionieren sollte, wenn Sie nur eine einfache, schnelle Problemumgehung wünschen.

Kenivia
quelle
Vielen Dank für Ihre Mühe. Ihre Lösung mag einfach sein, aber es ist ein einfacher Ausweg und nicht sehr praktisch. Um es noch einmal zu verdeutlichen: Ich suche nach einer Lösung, bei der Float-Werte korrekt auf eine beliebige Dezimalstelle "halbiert" werden. Ich bin gespannt, ob jemand anderes ein ähnliches Problem hatte und eine elegante und effektive Lösung dafür hat.
IcesHay
2
Diese Lösung ist falsch; Gibt beispielsweise round_half_up(1.499999999, 0)2.0 anstelle von 1.0 zurück.
user4815162342
@ user4815162342 es ist nicht so rund, normalerweise rundet 1.5 nach unten, weshalb op diese Frage an erster Stelle gestellt hat
Kenivia
@ user4815162342 aber ja, es ist in diesen Fällen nicht genau. Aber wenn Sie nur möchten, dass 1.5 abgerundet wird, denke ich, dass dies eine schöne schnelle Problemumgehung ist. Tolle Antwort von Ihrer Seite übrigens :)
Kenivia
1
Vielen Dank. Wenn Sie mich gestern gefragt hätten, hätte ich gesagt, dass die Rundung von 1.499999999 auf 2.0 sowohl allgemein als auch für das OP im Besonderen falsch war. Aber nach der letzten Diskussionsrunde mit dem OP in den Kommentaren zu meiner Antwort bin ich mir nicht mehr sicher und beginne zu denken, dass die Frage in ihrer aktuellen Form nicht beantwortbar ist. In diesem Licht widerrufe ich meine Ablehnung.
user4815162342