Skip to content
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

bpo-40801: Add operator.as_float #20481

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/operator.rst
Expand Up @@ -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)

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.10.rst
Expand Up @@ -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
-----------

Expand Down
22 changes: 22 additions & 0 deletions Lib/operator.py
Expand Up @@ -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__}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean real number in the math sense? Or do you mean it as instead of a string representation of a number?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the mathematical sense. But this wording isn't new to this PR; it's copied from here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That makes sense. :-)


return float(obj)

def floordiv(a, b):
"Same as a // b."
return a // b
Expand Down
81 changes: 81 additions & 0 deletions Lib/test/test_operator.py
Expand Up @@ -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

Expand All @@ -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

Expand Down
@@ -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.
45 changes: 45 additions & 0 deletions Modules/_operator.c
Expand Up @@ -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

Expand Down Expand Up @@ -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 */

};
Expand Down
28 changes: 27 additions & 1 deletion Modules/clinic/_operator.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.