diff options
author | Mark Dickinson <dickinsm@gmail.com> | 2010-04-03 11:08:14 (GMT) |
---|---|---|
committer | Mark Dickinson <dickinsm@gmail.com> | 2010-04-03 11:08:14 (GMT) |
commit | ac256ab2843dfb6c28af0227202df67664ed462e (patch) | |
tree | e4b82bd27da37911a04fde01cc06e7ddc39681df | |
parent | 5fc16b469ea96f1db11bb2fc9996ac056e8e1c3c (diff) | |
download | cpython-ac256ab2843dfb6c28af0227202df67664ed462e.zip cpython-ac256ab2843dfb6c28af0227202df67664ed462e.tar.gz cpython-ac256ab2843dfb6c28af0227202df67664ed462e.tar.bz2 |
Merged revisions 79583,79588-79589 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk
........
r79583 | mark.dickinson | 2010-04-02 09:53:22 +0100 (Fri, 02 Apr 2010) | 7 lines
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.
........
r79588 | mark.dickinson | 2010-04-02 11:17:07 +0100 (Fri, 02 Apr 2010) | 2 lines
Issue #7279: Make comparisons involving a Decimal sNaN signal InvalidOperation.
........
r79589 | mark.dickinson | 2010-04-02 11:35:12 +0100 (Fri, 02 Apr 2010) | 6 lines
Issue #7279: Make Decimal('nan') hashable. Decimal('snan') remains unhashable.
Also rewrite the Decimal __hash__ method so that it doesn't rely on
float('inf') being valid: float('inf') could raise an exception on
platforms not using IEEE 754 arithmetic.
........
-rw-r--r-- | Doc/library/decimal.rst | 18 | ||||
-rw-r--r-- | Lib/decimal.py | 67 | ||||
-rw-r--r-- | Lib/test/test_decimal.py | 87 | ||||
-rw-r--r-- | Misc/NEWS | 10 |
4 files changed, 155 insertions, 27 deletions
diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index 9d6ad81..8a806e9 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -358,6 +358,24 @@ Decimal objects compared, sorted, and coerced to another type (such as :class:`float` or :class:`int`). + 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 objects also have a number of specialized methods: diff --git a/Lib/decimal.py b/Lib/decimal.py index ab38ed4..727aee2 100644 --- a/Lib/decimal.py +++ b/Lib/decimal.py @@ -849,8 +849,11 @@ class Decimal(object): # subject of what should happen for a comparison involving a NaN. # We take the following approach: # - # == comparisons involving a NaN always return False - # != comparisons involving a NaN always return True + # == comparisons involving a quiet NaN always return False + # != comparisons involving a quiet NaN always return True + # == or != comparisons involving a signaling NaN signal + # InvalidOperation, and return False or True as above if the + # InvalidOperation is not trapped. # <, >, <= and >= comparisons involving a (quiet or signaling) # NaN signal InvalidOperation, and return False if the # InvalidOperation is not trapped. @@ -858,25 +861,25 @@ class Decimal(object): # This behavior is designed to conform as closely as possible to # that specified by IEEE 754. - def __eq__(self, other): - other = _convert_other(other) + def __eq__(self, other, context=None): + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other - if self.is_nan() or other.is_nan(): + if self._check_nans(other, context): return False return self._cmp(other) == 0 - def __ne__(self, other): - other = _convert_other(other) + def __ne__(self, other, context=None): + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other - if self.is_nan() or other.is_nan(): + if self._check_nans(other, context): return True return self._cmp(other) != 0 def __lt__(self, other, context=None): - other = _convert_other(other) + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other ans = self._compare_check_nans(other, context) @@ -885,7 +888,7 @@ class Decimal(object): return self._cmp(other) < 0 def __le__(self, other, context=None): - other = _convert_other(other) + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other ans = self._compare_check_nans(other, context) @@ -894,7 +897,7 @@ class Decimal(object): return self._cmp(other) <= 0 def __gt__(self, other, context=None): - other = _convert_other(other) + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other ans = self._compare_check_nans(other, context) @@ -903,7 +906,7 @@ class Decimal(object): return self._cmp(other) > 0 def __ge__(self, other, context=None): - other = _convert_other(other) + other = _convert_other(other, allow_float=True) if other is NotImplemented: return other ans = self._compare_check_nans(other, context) @@ -937,12 +940,34 @@ class Decimal(object): # The hash of a nonspecial noninteger Decimal must depend only # on the value of that Decimal, and not on its representation. # For example: hash(Decimal('100E-1')) == hash(Decimal('10')). + + # Equality comparisons involving signaling nans can raise an + # exception; since equality checks are implicitly and + # unpredictably used when checking set and dict membership, we + # prevent signaling nans from being used as set elements or + # dict keys by making __hash__ raise an exception. if self._is_special: - if self._isnan(): - raise TypeError('Cannot hash a NaN value.') - return hash(str(self)) - if not self: - return 0 + if self.is_snan(): + raise TypeError('Cannot hash a signaling NaN value.') + elif self.is_nan(): + # 0 to match hash(float('nan')) + return 0 + else: + # values chosen to match hash(float('inf')) and + # hash(float('-inf')). + if self._sign: + return -271828 + else: + return 314159 + + # In Python 2.7, we're allowing comparisons (but not + # arithmetic operations) between floats and Decimals; so if + # a Decimal instance is exactly representable as a float then + # its hash should match that of the float. + self_as_float = float(self) + if Decimal.from_float(self_as_float) == self: + return hash(self_as_float) + if self._isinteger(): op = _WorkRep(self.to_integral_value()) # to make computation feasible for Decimals with large @@ -5780,15 +5805,21 @@ def _log10_lb(c, correction = { ##### Helper Functions #################################################### -def _convert_other(other, raiseit=False): +def _convert_other(other, raiseit=False, allow_float=False): """Convert other to Decimal. 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): return other if isinstance(other, int): return Decimal(other) + if allow_float and isinstance(other, float): + return Decimal.from_float(other) + if raiseit: raise TypeError("Unable to convert %s to Decimal" % other) return NotImplemented diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 51bdf9c..7de2400 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -26,6 +26,7 @@ with the corresponding argument. import math import os, sys +import operator import pickle, copy import unittest from decimal import * @@ -1096,18 +1097,56 @@ class DecimalArithmeticOperatorsTest(unittest.TestCase): self.assertEqual(abs(Decimal(45)), abs(Decimal(-45))) # abs def test_nan_comparisons(self): + # comparisons involving signaling nans signal InvalidOperation + + # order comparisons (<, <=, >, >=) involving only quiet nans + # also signal InvalidOperation + + # equality comparisons (==, !=) involving only quiet nans + # don't signal, but return False or True respectively. + n = Decimal('NaN') s = Decimal('sNaN') i = Decimal('Inf') f = Decimal('2') - for x, y in [(n, n), (n, i), (i, n), (n, f), (f, n), - (s, n), (n, s), (s, i), (i, s), (s, f), (f, s), (s, s)]: - self.assertTrue(x != y) - self.assertTrue(not (x == y)) - self.assertTrue(not (x < y)) - self.assertTrue(not (x <= y)) - self.assertTrue(not (x > y)) - self.assertTrue(not (x >= y)) + + qnan_pairs = (n, n), (n, i), (i, n), (n, f), (f, n) + snan_pairs = (s, n), (n, s), (s, i), (i, s), (s, f), (f, s), (s, s) + order_ops = operator.lt, operator.le, operator.gt, operator.ge + equality_ops = operator.eq, operator.ne + + # results when InvalidOperation is not trapped + for x, y in qnan_pairs + snan_pairs: + for op in order_ops + equality_ops: + got = op(x, y) + expected = True if op is operator.ne else False + self.assertIs(expected, got, + "expected {0!r} for operator.{1}({2!r}, {3!r}); " + "got {4!r}".format( + expected, op.__name__, x, y, got)) + + # repeat the above, but this time trap the InvalidOperation + with localcontext() as ctx: + ctx.traps[InvalidOperation] = 1 + + for x, y in qnan_pairs: + for op in equality_ops: + got = op(x, y) + expected = True if op is operator.ne else False + self.assertIs(expected, got, + "expected {0!r} for " + "operator.{1}({2!r}, {3!r}); " + "got {4!r}".format( + expected, op.__name__, x, y, got)) + + for x, y in snan_pairs: + for op in equality_ops: + self.assertRaises(InvalidOperation, operator.eq, x, y) + self.assertRaises(InvalidOperation, operator.ne, x, y) + + for x, y in qnan_pairs + snan_pairs: + for op in order_ops: + self.assertRaises(InvalidOperation, op, x, y) def test_copy_sign(self): d = Decimal(1).copy_sign(Decimal(-2)) @@ -1213,6 +1252,23 @@ class DecimalUsabilityTest(unittest.TestCase): a.sort() self.assertEqual(a, b) + 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): d = Decimal('43.24') c = copy.copy(d) @@ -1223,6 +1279,10 @@ class DecimalUsabilityTest(unittest.TestCase): def test_hash_method(self): #just that it's hashable hash(Decimal(23)) + hash(Decimal('Infinity')) + hash(Decimal('-Infinity')) + hash(Decimal('nan123')) + hash(Decimal('-NaN')) test_values = [Decimal(sign*(2**m + n)) for m in [0, 14, 15, 16, 17, 30, 31, @@ -1257,10 +1317,19 @@ class DecimalUsabilityTest(unittest.TestCase): #the same hash that to an int self.assertEqual(hash(Decimal(23)), hash(23)) - self.assertRaises(TypeError, hash, Decimal('NaN')) + self.assertRaises(TypeError, hash, Decimal('sNaN')) 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 # current context (issue #1757) c = getcontext() @@ -301,6 +301,16 @@ C-API Library ------- +- Issue #7279: Comparisons involving a Decimal signaling NaN now + signal InvalidOperation instead of returning False. (Comparisons + involving a quiet NaN are unchanged.) Also, Decimal quiet NaNs + are now hashable; Decimal signaling NaNs remain unhashable. + +- 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). + - Added a subtract() method to collections.Counter(). - Issue #8233: When run as a script, py_compile.py optionally takes a single |