diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst index 36c53556c2685e..aeb7d9063b46ae 100644 --- a/Doc/library/operator.rst +++ b/Doc/library/operator.rst @@ -101,6 +101,15 @@ The mathematical and bitwise operations are the most numerous: Return the bitwise and of *a* and *b*. +.. function:: as_float(a) + + Return *a* converted to a float. Equivalent to ``float(a)``, except + that conversion from a string or bytestring is not permitted. The result + always has exact type :class:`float`. + + .. versionadded:: 3.10 + + .. function:: floordiv(a, b) __floordiv__(a, b) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 34a09fe4b505cc..04753c5abd2015 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -92,6 +92,13 @@ New Modules Improved Modules ================ +operator +-------- + +Added :func:`operator.as_float` to convert a numeric object to :class:`float`. +This exposes to Python a conversion whose semantics exactly match Python's +own implicit float conversions, for example as used in the :mod:`math` module. + tracemalloc ----------- diff --git a/Lib/operator.py b/Lib/operator.py index fb58851fa6ef67..0da0e02ca75c22 100644 --- a/Lib/operator.py +++ b/Lib/operator.py @@ -80,6 +80,28 @@ def and_(a, b): "Same as a & b." return a & b +def as_float(obj): + """ + Convert something numeric to float. + + Same as float(obj), but does not accept strings. + """ + # Exclude strings and anything exposing the buffer interface. + bad_type = False + if isinstance(obj, str): + bad_type = True + else: + try: + memoryview(obj) + bad_type = True + except TypeError: + pass + + if bad_type: + raise TypeError(f"must be real number, not {obj.__class__.__name__}") + + return float(obj) + def floordiv(a, b): "Same as a // b." return a // b diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index f46d94a226717a..c2922afaa86b53 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -499,6 +499,80 @@ def __length_hint__(self): with self.assertRaises(LookupError): operator.length_hint(X(LookupError)) + def test_as_float(self): + operator = self.module + + # Exact type float. + self.assertIsFloatWithValue(operator.as_float(2.3), 2.3) + + # Subclass of float. + class MyFloat(float): + pass + + self.assertIsFloatWithValue(operator.as_float(MyFloat(-1.56)), -1.56) + + # Non-float with a __float__ method. + class FloatLike: + def __float__(self): + return 1729.0 + + self.assertIsFloatWithValue(operator.as_float(FloatLike()), 1729.0) + + # Non-float with a __float__ method that returns an instance + # of a subclass of float. + class FloatLike2: + def __float__(self): + return MyFloat(918.0) + + self.assertIsFloatWithValue(operator.as_float(FloatLike2()), 918.0) + + # Plain old integer + self.assertIsFloatWithValue(operator.as_float(2), 2.0) + + # Integer subclass. + class MyInt(int): + pass + + self.assertIsFloatWithValue(operator.as_float(MyInt(-3)), -3.0) + + # Object supplying __index__ but not __float__. + class IntegerLike: + def __index__(self): + return 77 + + self.assertIsFloatWithValue(operator.as_float(IntegerLike()), 77.0) + + # Same as above, but with __index__ returning an instance of an + # int subclass. + class IntegerLike2: + def __index__(self): + return MyInt(78) + + self.assertIsFloatWithValue(operator.as_float(IntegerLike2()), 78.0) + + # Object with both __float__ and __index__; __float__ should take + # precedence. + class Confused: + def __float__(self): + return 123.456 + + def __index__(self): + return 123 + + self.assertIsFloatWithValue(operator.as_float(Confused()), 123.456) + + # Not convertible. + import array + + bad_values = [ + "123", b"123", bytearray(b"123"), None, 1j, + array.array('B', b"123.0"), + ] + for bad_value in bad_values: + with self.subTest(bad_value=bad_value): + with self.assertRaises(TypeError): + operator.as_float(bad_value) + def test_dunder_is_original(self): operator = self.module @@ -509,6 +583,13 @@ def test_dunder_is_original(self): if dunder: self.assertIs(dunder, orig) + def assertIsFloatWithValue(self, actual, expected): + self.assertIs(type(actual), float) + # Compare reprs rather than values, to deal correctly with corner + # cases like nans and signed zeros. + self.assertEqual(repr(actual), repr(expected)) + + class PyOperatorTestCase(OperatorTestCase, unittest.TestCase): module = py_operator diff --git a/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst b/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst new file mode 100644 index 00000000000000..efafd94c583a9b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst @@ -0,0 +1,6 @@ +Add a new :func:`operator.as_float` function for converting an arbitrary +Python numeric object to :class:`float`. This is a simple wrapper around the +:c:func:`PyFloat_AsDouble` C-API function. The intent is to provide at +Python level a conversion whose semantics exactly match Python's own +implicit float conversions, for example those used in the :mod:`math` +module. diff --git a/Modules/_operator.c b/Modules/_operator.c index 8a54829e5bbcc8..4aa4d2311617f7 100644 --- a/Modules/_operator.c +++ b/Modules/_operator.c @@ -761,6 +761,50 @@ _tscmp(const unsigned char *a, const unsigned char *b, return (result == 0); } +/*[clinic input] +_operator.as_float -> + + obj: object + / + +Return *obj* interpreted as a float. + +If *obj* is already of exact type float, return it unchanged. + +If *obj* is already an instance of float (including possibly an instance of a +float subclass), return a float with the same value as *obj*. + +If *obj* is not an instance of float but its type has a __float__ method, use +that method to convert *obj* to a float. + +If *obj* is not an instance of float and its type does not have a __float__ +method but does have an __index__ method, use that method to +convert *obj* to an integer, and then convert that integer to a float. + +If *obj* cannot be converted to a float, raise TypeError. + +Calling as_float is equivalent to calling *float* directly, except that string +objects are not accepted. + +[clinic start generated code]*/ + +static PyObject * +_operator_as_float(PyObject *module, PyObject *obj) +/*[clinic end generated code: output=25b27903bbd14913 input=39db64b91327f393]*/ +{ + if (PyFloat_CheckExact(obj)) { + Py_INCREF(obj); + return obj; + } + + double x = PyFloat_AsDouble(obj); + if (x == -1.0 && PyErr_Occurred()) { + return NULL; + } + return PyFloat_FromDouble(x); +} + + /*[clinic input] _operator.length_hint -> Py_ssize_t @@ -929,6 +973,7 @@ static struct PyMethodDef operator_methods[] = { _OPERATOR_GE_METHODDEF _OPERATOR__COMPARE_DIGEST_METHODDEF _OPERATOR_LENGTH_HINT_METHODDEF + _OPERATOR_AS_FLOAT_METHODDEF {NULL, NULL} /* sentinel */ }; diff --git a/Modules/clinic/_operator.c.h b/Modules/clinic/_operator.c.h index 34b6fdadfb7309..5935fd9d5e1bfe 100644 --- a/Modules/clinic/_operator.c.h +++ b/Modules/clinic/_operator.c.h @@ -1390,6 +1390,32 @@ _operator_is_not(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } +PyDoc_STRVAR(_operator_as_float__doc__, +"as_float($module, obj, /)\n" +"--\n" +"\n" +"Return *obj* interpreted as a float.\n" +"\n" +"If *obj* is already of exact type float, return it unchanged.\n" +"\n" +"If *obj* is already an instance of float (including possibly an instance of a\n" +"float subclass), return a float with the same value as *obj*.\n" +"\n" +"If *obj* is not an instance of float but its type has a __float__ method, use\n" +"that method to convert *obj* to a float.\n" +"\n" +"If *obj* is not an instance of float and its type does not have a __float__\n" +"method but does have an __index__ method, use that method to\n" +"convert *obj* to an integer, and then convert that integer to a float.\n" +"\n" +"If *obj* cannot be converted to a float, raise TypeError.\n" +"\n" +"Calling as_float is equivalent to calling *float* directly, except that string\n" +"objects are not accepted."); + +#define _OPERATOR_AS_FLOAT_METHODDEF \ + {"as_float", (PyCFunction)_operator_as_float, METH_O, _operator_as_float__doc__}, + PyDoc_STRVAR(_operator_length_hint__doc__, "length_hint($module, obj, default=0, /)\n" "--\n" @@ -1486,4 +1512,4 @@ _operator__compare_digest(PyObject *module, PyObject *const *args, Py_ssize_t na exit: return return_value; } -/*[clinic end generated code: output=eae5d08f971a65fd input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5a828b48c9f80dbf input=a9049054013a1b77]*/