-
-
Notifications
You must be signed in to change notification settings - Fork 31.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decimal strict mode should prevent Decimal("0.89") == 0.89 #125557
Comments
Not at all: Otherwise (the signal is trapped), only equality comparisons and explicit conversions are silent. All other mixed operations raise FloatOperation."
Why do you think it's a wrong answer?! 1/10 != (binary approximation of)0.1; the 0.1 can't be represented exactly as binary floating-point number. See https://docs.python.org/3/tutorial/floatingpoint.html#floating-point-arithmetic-issues-and-limitations So, current behaviour seems to be consistent and well documented. Your only argument against is wrong. And this will break backward compatibility. To change things you need more arguments. Probably, this should be discussed first on https://discuss.python.org/ |
There's a more general design principle at work here (though it's not one I think I've seen articulated clearly in the docs), not related to If >>> from decimal import Decimal
>>> s = {Decimal("0.47012"), 8946670875749133.0}
>>> list(map(hash, s))
[8946670875749133, 8946670875749133] |
Perhaps, most close to documenting it is a quote from here: "When no appropriate method returns any value other than NotImplemented, the == and != operators will fall back to is and is not, respectively." It doesn't say why this default does exists, however. But I'm not sure it worth. I think this can be closed. |
The general design principle is that floating point strict mode should prevent coders from making mistakes. Sure it's not always a mistake to say >>> Decimal(0.89)
Decimal('0.89000000000000001332267629550187848508358001708984375') Decimal has lots of use cases, and a common one is to handle dollars-and-cents. If you say account.balance < amount You can run into trouble because >>> Decimal("0.89") < 0.89
True This sort of problem is flagged by turning on floating point strict mode, and the runtime will flag that statement as incorrect. The exact same argument applies to equality. For many use cases, equality between Dollar and float is a coding error, and floating point strict mode should catch it. The idea that things like sets use equality implicitly is interesting. Problem is, most people would be surprised by >>> 0.89 in set([Decimal("0.89")])
False What is the use case where equality makes sense between Decimal and float? As for breaking changes, we could introduce a new flag, StrictMode, which would do what FloatOperation does, but includes equality. |
When they are mathematically equal.
We don't loose anything, unless you apply context settings with insufficient precision:
Mark remind us above why we can't raise an exception here. So, in this case you suggest to fall back on |
I wasn't suggesting that we fall back on To your point, I ask, what is the use case for saying |
Then what? Currently Decimal's comparison method does implicit conversion and then do comparison. We have options: 1) implement comparison method in some other way (how?) or 2) just return If you rule out 2) - please explain how we should implement 1) instead.
To convert losslessly binary floating-point number to it's Decimal equivalent. Or in other words: to construct Decimal instances from some binary fractions (
It depends. To achieve the previous goal e.g. for >>> x = 0.1
>>> with localcontext() as ctx:
... ctx.prec = 55 # assuming IEEE doubles
... n, d = x.as_integer_ratio()
... a = Decimal(n)/d
...
>>> a # == Decimal(x)
Decimal('0.1000000000000000055511151231257827021181583404541015625') Using a string in the Decimal constructor - the correct way to enter a decimal fraction. |
Elaborating on what @mdickinson said, there's an even more general design principle at work: starting with the introduction of the classes in the It was appreciated at the time that this would simplify reasoning about sets and dicts, but the latter isn't really what drove it. Instead the latter was taken as confirmation of that it was a Good Idea in general. |
Other than raising an exception (that, I think, excluded by Mark's arguments) - the only option is to make a context-dependent equality. I think this is silly, we might end up e.g. with a set, that after changing context settings - has equal elements. |
@mdickinson wrote:
Perhaps when f = 0.0
d = Decimal(0)
if f == d:
# whatever That works fine today regardless of In any case, behavior that's working as designed and documented isn't properly called "a bug" in the issue tracker. I suggest changing it to a feature request, and instead ask for a new exception to be introduced. one that acts like |
@skirpichev, I asked what is the use case for @mdickinson points out that There is a reason that Decimal exists, and it is to allow decimal computations without worrying about floating point rounding. If you care about that then you would use Decimal and avoid float. Mixing the two leads to incorrect answers. Raising on equality would prevent that. Put a different way, IF you want to mix Decimal and float, then why would you want to prevent comparing Decimal < float ? What is the purpose of floating point strict mode? If you want to mix Decimal and float, don't turn on strict mode. The compromise that is strict mode now, it isn't useful. |
It's not your use case, but I've frequently done such things, because the ability is quite useful for people analyzing floating-point error propagation. Read the room here? For reasons of backward compatibility alone, I expect there's essentially no chance this will change. But I think you could make a decent case for adding a new exception (say,
It doesn't do everything you want, but does do a great deal of you want. It does do everything I want of it (and I do use it when appropriate), but then I'm always acutely aware of how mixing numeric types works in Python. "Isn't useful" is too strong. |
Well, just showing the decimal floating-point number, which is equal to the given binary floating-point number - is a useful thing on itself. People too often misinterpret floating-point literals like (IMO, "short" float repr in 2.7+ and 3.1+ rather adds more confusion here.) And too often bugs "this Python answer in that floating-point computation is wrong/not accurate/imprecise" ends with similar computations with Decimal equivalents. Just a quick examples : #111933 (comment) ("python answer is wrong!") or #82884 (comment) (a subtype, "round()'s answer is wrong").
They aren't really got mixed. Floats are converted to decimal equivalents.
I'm afraid, but you can't in general avoid floating-point rounding in computations with decimal numbers (try to trap the Inexact signal and see how long you can play with numbers).
E.g. to prevent comparisons like >>> Decimal('0.200000000000000001') <= 0.2
True On another hand, if you are using I've changed this to a feature request per Tim's suggestion. But I don't see much sense in this. |
@skirpichev, I'm sympathetic to the OP"s complaint. I don't know the history of To a non-expert, this all seems arbitrary, and inessential arbitrariness is "unPythonic". A new exception that says "no implicit mixing of decimals and floats, period" would at least be comprehensible to everyone. Although, as Mark said, opens the door to new surprises (due to Python implicitly doing equality comparisons under the covers, for dicts and sets, and even for |
I'll add that one non-obvious advantage of the current default behavior is that it allows lists mixing floats and Decimals to be sorted without exception, and likewise to be used by |
If you want to mix Decimal and float, then you wouldn't turn on floating-point-strict-mode. We are back to the question, what is the point of floating-point-strict-mode? As for using Decimal to display what's going on with floating point representation, again, don't turn on floating-point-strict-mode. Also there is a much more direct way to do that: >>> f"{0.1:.60f}"
'0.100000000000000005551115123125782702118158340454101562500000' Does anybody have an application that has floating-point-strict-mode turned on, where you want it to allow equality between Decimal and float? That's the use case I am looking for. If you can't come up with a use case, then you are saying, "It's the correct behavior because it's the actual behavior." |
Inadequate in general.
Misses the real point. "It's the actual behavior" alone is entirely what "backward compatibility" is about. Python is exceedingly reluctant to introduce breaking changes, and especially not so when "the actual behavior" is the documented behavior. It remains possible that a new exception could be introduced that would do what you want. |
@tim-one, honestly I didn't get Mark's point on why it shouldn't be extended to arithmetic. See #87768. But this is another story.
And it will be much less useful. IMO, using On another hand,
@timkay, you repeat question, which was answered above. |
I don't see value in revisiting old decisions. It's messy, and can't change what we're stuck with now 😉.
It's too late to change the current
We don't want to forbid it. But under the new exception, it would raise an exception, as an unavoidable consequence of making (say)
And it's worse than just that. In non-trivial examples, whether an exception is raised doesn't just depend on the values in the set (or dict), but also on the history of insertions and deletions. Nothing whatsoever is defined about the order in which hash chain collisions are traversed. As Mark said, "equality checks are performed implicitly and unpredictably in hash-based collections like dict and set". I've never mixed Decimals with floats in a set or dict to begin with, so would never notice. If I ever did, and got burned, I'd have to stop enabling the new exception. Fine by me. |
Yet I have tried to dig in history with hope to understand motivations for the current design. But without much luck. It seems, the
And even worse, on whether you have enabled >>> import _pydecimal as decimal
>>> l2 = [decimal.Decimal("0.47012"), 8946670875749133.0]
>>> s2 = set(l2)
>>> l2.index(8946670875749133.0)
1
>>> decimal.getcontext().traps[decimal.FloatOperation] = True # "forbidden" containers can't be created below
>>> l2.index(8946670875749133.0)
Traceback (most recent call last):
...
decimal.FloatOperation: strict semantics for mixing floats and Decimals are enabled
>>> l2.index(decimal.Decimal("0.47012"))
0
>>> 1.1 in s2
False
>>> 8946670875749133.0 in s2
Traceback (most recent call last):
...
decimal.FloatOperation: strict semantics for mixing floats and Decimals are enabled a patch to playdiff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py
index ec03619933..a2dbd3059f 100644
--- a/Lib/_pydecimal.py
+++ b/Lib/_pydecimal.py
@@ -807,7 +807,7 @@ def _cmp(self, other):
# that specified by IEEE 754.
def __eq__(self, other, context=None):
- self, other = _convert_for_comparison(self, other, equality_op=True)
+ self, other = _convert_for_comparison(self, other, equality_op=False)
if other is NotImplemented:
return other
if self._check_nans(other, context): On another hand, with enabled In either case, I doubt that new semantics of the
Hmm, can't we treat the current behavior as a bug? It's trapping turned off by default. |
I don't see anything new here. The current behavior is working as designed and documented, so can't be called "a bug". You could call it a "design error", but I wouldn't. At worst it's a design decision the OP doesn't like. For more on that, you'd have to ask @skrah. The mountain of code is overwhelmingly his, and best I can tell Sorting is a red herring. Sorting doesn't use >>> import decimal as d
>>> d.getcontext().traps[d.FloatOperation] = True
>>> sorted([d.Decimal("1"), 1.0])
Traceback (most recent call last):
File "<python-input-4>", line 1, in <module>
sorted([d.Decimal("1"), 1.0])
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
decimal.FloatOperation: [<class 'decimal.FloatOperation'>] Users don't appreciate breaking changes, so I oppose changing the currently well-defined semantics in any way. But I may weakly support adding a new, "even stricter" trap, which also complained about mixed (in)equality comparisons. That's only "may support", because, as mentioned before, a deeper design invariant is that In the case of mixing floats and decimals, It is, after all, a plan fact that |
Not quite. While sorting itself never does (in)equality comparison directly, applying |
What to do with mixed float/decimal comparisons has a long, contentious history, going back before Python 3, and including a span when didn't it complain but returned nonsense results: But there's essentially no public discussion I found of Throughout he didn't care much about sorting, but about that And the design of So I'm satisficed it's working as intended, I'd leave it alone. -1 on changing its semantics in any way, and -1 on deprecating it. At best +0 on introducing a stricter trap to prevent mixed (in)equality comparison too. And I'll leave it there. If someone else around at the time (@mdickinson , @rhettinger) doesn't chime in with a strong opposing preference, I expect to just eventually close this as "not planned". |
Ok, lets see of some core dev(s) will in favor of this (i.e. Lets not forget, that in principle we have access to contexts flags even if a signal isn't trapped: >>> import decimal
>>> ctx = decimal.getcontext()
>>> [k.__name__ for k, v in ctx.flags.items() if v]
[]
>>> d = decimal.Decimal(0.1)
>>> [k.__name__ for k, v in ctx.flags.items() if v]
['FloatOperation']
>>> d + 1
Decimal('1.100000000000000005551115123')
>>> [k.__name__ for k, v in ctx.flags.items() if v]
['FloatOperation', 'Inexact', 'Rounded'] |
I cannot add anything new. @mdickinson's argument looks solid to me. |
Bug report
Bug description:
Mixing Decimals and floats will often get you the wrong answer. By default, the Decimal library allows such behavior:
Fortunately, you can turn on floating point strict mode, where mixing Decimals and floats is prohibited:
HOWEVER, for some reason,
==
and!=
are allowed in floating point strict mode, and produce wrong answers:When floating point strict mode is on, why is
==
and!=
still allowed?This code would be better without the
if equality_op:
:Why are
==
and!=
allowed in floating point strict mode?CPython versions tested on:
3.12
Operating systems tested on:
Linux
The text was updated successfully, but these errors were encountered: