[Python-checkins] r79668 - in python/branches/py3k: Doc/library/decimal.rst Lib/decimal.py Lib/test/test_decimal.py Misc/NEWS

mark.dickinson python-checkins at python.org
Sat Apr 3 13:08:14 CEST 2010


Author: mark.dickinson
Date: Sat Apr  3 13:08:14 2010
New Revision: 79668

Log:
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.
........


Modified:
   python/branches/py3k/   (props changed)
   python/branches/py3k/Doc/library/decimal.rst
   python/branches/py3k/Lib/decimal.py
   python/branches/py3k/Lib/test/test_decimal.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/library/decimal.rst
==============================================================================
--- python/branches/py3k/Doc/library/decimal.rst	(original)
+++ python/branches/py3k/Doc/library/decimal.rst	Sat Apr  3 13:08:14 2010
@@ -358,6 +358,24 @@
    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:
 

Modified: python/branches/py3k/Lib/decimal.py
==============================================================================
--- python/branches/py3k/Lib/decimal.py	(original)
+++ python/branches/py3k/Lib/decimal.py	Sat Apr  3 13:08:14 2010
@@ -849,8 +849,11 @@
     # 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 @@
     # 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 @@
         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 @@
         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 @@
         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 @@
         # 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 @@
 
 ##### 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

Modified: python/branches/py3k/Lib/test/test_decimal.py
==============================================================================
--- python/branches/py3k/Lib/test/test_decimal.py	(original)
+++ python/branches/py3k/Lib/test/test_decimal.py	Sat Apr  3 13:08:14 2010
@@ -26,6 +26,7 @@
 
 import math
 import os, sys
+import operator
 import pickle, copy
 import unittest
 from decimal import *
@@ -1096,18 +1097,56 @@
         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 @@
         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 @@
     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 @@
 
         #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()

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Sat Apr  3 13:08:14 2010
@@ -301,6 +301,16 @@
 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


More information about the Python-checkins mailing list