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 1 commit
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
7 changes: 7 additions & 0 deletions Doc/library/operator.rst
Expand Up @@ -101,6 +101,13 @@ 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 an float. Equivalent to ``float(a)``, except
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
that conversion from a string or bytestring is not permitted. The result
always has exact type :class:`float`.


.. 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
10 changes: 10 additions & 0 deletions Lib/operator.py
Expand Up @@ -80,6 +80,16 @@ 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.
"""
if isinstance(obj, (str, bytes, bytearray)):
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
raise TypeError("as_float argument must be numeric")
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
return float(obj)

def floordiv(a, b):
"Same as a // b."
return a // b
Expand Down
76 changes: 76 additions & 0 deletions Lib/test/test_operator.py
Expand Up @@ -499,6 +499,75 @@ 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.
bad_values = ["123", b"123", bytearray(b"123"), None, 1j]
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 +578,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.