mirror of https://github.com/python/cpython
Issue #2531: Make float-to-decimal comparisons return correct results.
Float to decimal comparison operations now return a result based on the numeric values of the operands. Decimal.__hash__ has also been fixed so that Decimal and float values that compare equal have equal hash value.
This commit is contained in:
parent
6eba779235
commit
99d8096c17
|
@ -364,6 +364,24 @@ Decimal objects
|
||||||
compared, sorted, and coerced to another type (such as :class:`float` or
|
compared, sorted, and coerced to another type (such as :class:`float` or
|
||||||
:class:`long`).
|
:class:`long`).
|
||||||
|
|
||||||
|
Decimal objects cannot generally be combined with floats in
|
||||||
|
arithmetic operations: an attempt to add a :class:`Decimal` to a
|
||||||
|
:class:`float`, for example, will raise a :exc:`TypeError`.
|
||||||
|
There's one exception to this rule: it's possible to use Python's
|
||||||
|
comparison operators to compare a :class:`float` instance ``x``
|
||||||
|
with a :class:`Decimal` instance ``y``. Without this exception,
|
||||||
|
comparisons between :class:`Decimal` and :class:`float` instances
|
||||||
|
would follow the general rules for comparing objects of different
|
||||||
|
types described in the :ref:`expressions` section of the reference
|
||||||
|
manual, leading to confusing results.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.7
|
||||||
|
A comparison between a :class:`float` instance ``x`` and a
|
||||||
|
:class:`Decimal` instance ``y`` now returns a result based on
|
||||||
|
the values of ``x`` and ``y``. In earlier versions ``x < y``
|
||||||
|
returned the same (arbitrary) result for any :class:`Decimal`
|
||||||
|
instance ``x`` and any :class:`float` instance ``y``.
|
||||||
|
|
||||||
In addition to the standard numeric properties, decimal floating point
|
In addition to the standard numeric properties, decimal floating point
|
||||||
objects also have a number of specialized methods:
|
objects also have a number of specialized methods:
|
||||||
|
|
||||||
|
|
|
@ -855,7 +855,7 @@ class Decimal(object):
|
||||||
# that specified by IEEE 754.
|
# that specified by IEEE 754.
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
if self.is_nan() or other.is_nan():
|
if self.is_nan() or other.is_nan():
|
||||||
|
@ -863,7 +863,7 @@ class Decimal(object):
|
||||||
return self._cmp(other) == 0
|
return self._cmp(other) == 0
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
if self.is_nan() or other.is_nan():
|
if self.is_nan() or other.is_nan():
|
||||||
|
@ -871,7 +871,7 @@ class Decimal(object):
|
||||||
return self._cmp(other) != 0
|
return self._cmp(other) != 0
|
||||||
|
|
||||||
def __lt__(self, other, context=None):
|
def __lt__(self, other, context=None):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
ans = self._compare_check_nans(other, context)
|
ans = self._compare_check_nans(other, context)
|
||||||
|
@ -880,7 +880,7 @@ class Decimal(object):
|
||||||
return self._cmp(other) < 0
|
return self._cmp(other) < 0
|
||||||
|
|
||||||
def __le__(self, other, context=None):
|
def __le__(self, other, context=None):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
ans = self._compare_check_nans(other, context)
|
ans = self._compare_check_nans(other, context)
|
||||||
|
@ -889,7 +889,7 @@ class Decimal(object):
|
||||||
return self._cmp(other) <= 0
|
return self._cmp(other) <= 0
|
||||||
|
|
||||||
def __gt__(self, other, context=None):
|
def __gt__(self, other, context=None):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
ans = self._compare_check_nans(other, context)
|
ans = self._compare_check_nans(other, context)
|
||||||
|
@ -898,7 +898,7 @@ class Decimal(object):
|
||||||
return self._cmp(other) > 0
|
return self._cmp(other) > 0
|
||||||
|
|
||||||
def __ge__(self, other, context=None):
|
def __ge__(self, other, context=None):
|
||||||
other = _convert_other(other)
|
other = _convert_other(other, allow_float=True)
|
||||||
if other is NotImplemented:
|
if other is NotImplemented:
|
||||||
return other
|
return other
|
||||||
ans = self._compare_check_nans(other, context)
|
ans = self._compare_check_nans(other, context)
|
||||||
|
@ -932,12 +932,18 @@ class Decimal(object):
|
||||||
# The hash of a nonspecial noninteger Decimal must depend only
|
# The hash of a nonspecial noninteger Decimal must depend only
|
||||||
# on the value of that Decimal, and not on its representation.
|
# on the value of that Decimal, and not on its representation.
|
||||||
# For example: hash(Decimal('100E-1')) == hash(Decimal('10')).
|
# For example: hash(Decimal('100E-1')) == hash(Decimal('10')).
|
||||||
if self._is_special:
|
if self._is_special and self._isnan():
|
||||||
if self._isnan():
|
raise TypeError('Cannot hash a NaN value.')
|
||||||
raise TypeError('Cannot hash a NaN value.')
|
|
||||||
return hash(str(self))
|
# In Python 2.7, we're allowing comparisons (but not
|
||||||
if not self:
|
# arithmetic operations) between floats and Decimals; so if
|
||||||
return 0
|
# a Decimal instance is exactly representable as a float then
|
||||||
|
# its hash should match that of the float. Note that this takes care
|
||||||
|
# of zeros and infinities, as well as small integers.
|
||||||
|
self_as_float = float(self)
|
||||||
|
if Decimal.from_float(self_as_float) == self:
|
||||||
|
return hash(self_as_float)
|
||||||
|
|
||||||
if self._isinteger():
|
if self._isinteger():
|
||||||
op = _WorkRep(self.to_integral_value())
|
op = _WorkRep(self.to_integral_value())
|
||||||
# to make computation feasible for Decimals with large
|
# to make computation feasible for Decimals with large
|
||||||
|
@ -5695,15 +5701,21 @@ def _log10_lb(c, correction = {
|
||||||
|
|
||||||
##### Helper Functions ####################################################
|
##### Helper Functions ####################################################
|
||||||
|
|
||||||
def _convert_other(other, raiseit=False):
|
def _convert_other(other, raiseit=False, allow_float=False):
|
||||||
"""Convert other to Decimal.
|
"""Convert other to Decimal.
|
||||||
|
|
||||||
Verifies that it's ok to use in an implicit construction.
|
Verifies that it's ok to use in an implicit construction.
|
||||||
|
If allow_float is true, allow conversion from float; this
|
||||||
|
is used in the comparison methods (__eq__ and friends).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(other, Decimal):
|
if isinstance(other, Decimal):
|
||||||
return other
|
return other
|
||||||
if isinstance(other, (int, long)):
|
if isinstance(other, (int, long)):
|
||||||
return Decimal(other)
|
return Decimal(other)
|
||||||
|
if allow_float and isinstance(other, float):
|
||||||
|
return Decimal.from_float(other)
|
||||||
|
|
||||||
if raiseit:
|
if raiseit:
|
||||||
raise TypeError("Unable to convert %s to Decimal" % other)
|
raise TypeError("Unable to convert %s to Decimal" % other)
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
|
@ -1208,6 +1208,23 @@ class DecimalUsabilityTest(unittest.TestCase):
|
||||||
self.assertFalse(Decimal(1) < None)
|
self.assertFalse(Decimal(1) < None)
|
||||||
self.assertTrue(Decimal(1) > None)
|
self.assertTrue(Decimal(1) > None)
|
||||||
|
|
||||||
|
def test_decimal_float_comparison(self):
|
||||||
|
da = Decimal('0.25')
|
||||||
|
db = Decimal('3.0')
|
||||||
|
self.assert_(da < 3.0)
|
||||||
|
self.assert_(da <= 3.0)
|
||||||
|
self.assert_(db > 0.25)
|
||||||
|
self.assert_(db >= 0.25)
|
||||||
|
self.assert_(da != 1.5)
|
||||||
|
self.assert_(da == 0.25)
|
||||||
|
self.assert_(3.0 > da)
|
||||||
|
self.assert_(3.0 >= da)
|
||||||
|
self.assert_(0.25 < db)
|
||||||
|
self.assert_(0.25 <= db)
|
||||||
|
self.assert_(0.25 != db)
|
||||||
|
self.assert_(3.0 == db)
|
||||||
|
self.assert_(0.1 != Decimal('0.1'))
|
||||||
|
|
||||||
def test_copy_and_deepcopy_methods(self):
|
def test_copy_and_deepcopy_methods(self):
|
||||||
d = Decimal('43.24')
|
d = Decimal('43.24')
|
||||||
c = copy.copy(d)
|
c = copy.copy(d)
|
||||||
|
@ -1256,6 +1273,15 @@ class DecimalUsabilityTest(unittest.TestCase):
|
||||||
self.assertTrue(hash(Decimal('Inf')))
|
self.assertTrue(hash(Decimal('Inf')))
|
||||||
self.assertTrue(hash(Decimal('-Inf')))
|
self.assertTrue(hash(Decimal('-Inf')))
|
||||||
|
|
||||||
|
# check that the hashes of a Decimal float match when they
|
||||||
|
# represent exactly the same values
|
||||||
|
test_strings = ['inf', '-Inf', '0.0', '-.0e1',
|
||||||
|
'34.0', '2.5', '112390.625', '-0.515625']
|
||||||
|
for s in test_strings:
|
||||||
|
f = float(s)
|
||||||
|
d = Decimal(s)
|
||||||
|
self.assertEqual(hash(f), hash(d))
|
||||||
|
|
||||||
# check that the value of the hash doesn't depend on the
|
# check that the value of the hash doesn't depend on the
|
||||||
# current context (issue #1757)
|
# current context (issue #1757)
|
||||||
c = getcontext()
|
c = getcontext()
|
||||||
|
|
|
@ -35,6 +35,11 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #2531: Comparison operations between floats and Decimal
|
||||||
|
instances now return a result based on the numeric values of the
|
||||||
|
operands; previously they returned an arbitrary result based on
|
||||||
|
the relative ordering of id(float) and id(Decimal).
|
||||||
|
|
||||||
- Issue #8233: When run as a script, py_compile.py optionally takes a single
|
- Issue #8233: When run as a script, py_compile.py optionally takes a single
|
||||||
argument `-` which tells it to read files to compile from stdin. Each line
|
argument `-` which tells it to read files to compile from stdin. Each line
|
||||||
is read on demand and the named file is compiled immediately. (Original
|
is read on demand and the named file is compiled immediately. (Original
|
||||||
|
|
Loading…
Reference in New Issue