Skip to content

gh-84978: Add float.from_number() and complex.from_number() #26827

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

Merged
merged 9 commits into from
Jul 15, 2024
4 changes: 4 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ are always available. They are listed here in alphabetical order.
If one of arguments is a real number, only its real component is used in
the above expressions.

See also :meth:`complex.from_number` which only accepts a single numeric argument.

If all arguments are omitted, returns ``0j``.

The complex type is described in :ref:`typesnumeric`.
Expand Down Expand Up @@ -788,6 +790,8 @@ are always available. They are listed here in alphabetical order.
``x.__float__()``. If :meth:`~object.__float__` is not defined then it falls back
to :meth:`~object.__index__`.

See also :meth:`float.from_number` which only accepts a numeric argument.

If no argument is given, ``0.0`` is returned.

The float type is described in :ref:`typesnumeric`.
Expand Down
36 changes: 36 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,23 @@ Additional Methods on Float
The float type implements the :class:`numbers.Real` :term:`abstract base
class`. float also has the following additional methods.

.. classmethod:: float.from_number(x)

Class method to return a floating point number constructed from a number *x*.
Copy link
Member

Choose a reason for hiding this comment

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

Super-nitpick: I prefer the spelling floating-point number, with a hyphen (the "floating-point" part acts as a compound adjective). I think that's what we mostly use throughout the docs (though no doubt there are exceptions).

Copy link
Member Author

Choose a reason for hiding this comment

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

This descriptions was simply copied from the float constructor description.

"floating-point" occurs 118 time, "floating point" occurs at least 159 times (in several cases it is split between lines, so it is not easy to get accurate number).

So for now I leave "floating point" for consistency with the constructor description. We will solve this in a separate issue.


If the argument is an integer or a floating point number, a
floating point number with the same value (within Python's floating point
precision) is returned. If the argument is outside the range of a Python
float, an :exc:`OverflowError` will be raised.

For a general Python object ``x``, ``float.from_number(x)`` delegates to
``x.__float__()``.
If :meth:`~object.__float__` is not defined then it falls back
to :meth:`~object.__index__`.

.. versionadded:: 3.14


.. method:: float.as_integer_ratio()

Return a pair of integers whose ratio is exactly equal to the
Expand Down Expand Up @@ -703,6 +720,25 @@ hexadecimal string representing the same number::
'0x1.d380000000000p+11'


Additional Methods on Complex
-----------------------------

The :class:`!complex` type implements the :class:`numbers.Complex`
:term:`abstract base class`.
:class:`!complex` also has the following additional methods.

.. classmethod:: complex.from_number(x)

Class method to convert a number to a complex number.

For a general Python object ``x``, ``complex.from_number(x)`` delegates to
``x.__complex__()``. If :meth:`~object.__complex__` is not defined then it falls back
to :meth:`~object.__float__`. If :meth:`!__float__` is not defined then it falls back
to :meth:`~object.__index__`.

.. versionadded:: 3.14


.. _numeric-hash:

Hashing of numeric types
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ New Features
Other Language Changes
======================

* Added class methods :meth:`float.from_number` and :meth:`complex.from_number`
to convert a number to :class:`float` or :class:`complex` type correspondingly.
They raise an error if the argument is a string.
(Contributed by Serhiy Storchaka in :gh:`84978`.)


New Modules
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ def __float__(self):
class ComplexSubclass(complex):
pass

class OtherComplexSubclass(complex):
pass

class MyInt:
def __init__(self, value):
self.value = value

def __int__(self):
return self.value

class WithComplex:
def __init__(self, value):
self.value = value
Expand Down Expand Up @@ -675,6 +685,35 @@ def test_underscores(self):
if not any(ch in lit for ch in 'xXoObB'):
self.assertRaises(ValueError, complex, lit)

def test_from_number(self, cls=complex):
def eq(actual, expected):
self.assertEqual(actual, expected)
self.assertIs(type(actual), cls)

eq(cls.from_number(3.14), 3.14+0j)
eq(cls.from_number(3.14j), 3.14j)
eq(cls.from_number(314), 314.0+0j)
eq(cls.from_number(OtherComplexSubclass(3.14, 2.72)), 3.14+2.72j)
eq(cls.from_number(WithComplex(3.14+2.72j)), 3.14+2.72j)
eq(cls.from_number(WithFloat(3.14)), 3.14+0j)
eq(cls.from_number(WithIndex(314)), 314.0+0j)

cNAN = complex(NAN, NAN)
x = cls.from_number(cNAN)
self.assertTrue(x != x)
self.assertIs(type(x), cls)
if cls is complex:
self.assertIs(cls.from_number(cNAN), cNAN)

self.assertRaises(TypeError, cls.from_number, '3.14')
self.assertRaises(TypeError, cls.from_number, b'3.14')
self.assertRaises(TypeError, cls.from_number, MyInt(314))
self.assertRaises(TypeError, cls.from_number, {})
self.assertRaises(TypeError, cls.from_number)

def test_from_number_subclass(self):
self.test_from_number(ComplexSubclass)

def test_hash(self):
for x in range(-30, 30):
self.assertEqual(hash(x), hash(complex(x, 0)))
Expand Down
89 changes: 61 additions & 28 deletions Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ class FloatSubclass(float):
class OtherFloatSubclass(float):
pass

class MyIndex:
def __init__(self, value):
self.value = value

def __index__(self):
return self.value

class MyInt:
def __init__(self, value):
self.value = value

def __int__(self):
return self.value

class FloatLike:
def __init__(self, value):
self.value = value

def __float__(self):
return self.value


class GeneralFloatCases(unittest.TestCase):

def test_float(self):
Expand Down Expand Up @@ -181,10 +203,6 @@ def test_float_with_comma(self):

def test_floatconversion(self):
# Make sure that calls to __float__() work properly
class Foo1(object):
def __float__(self):
return 42.

class Foo2(float):
def __float__(self):
return 42.
Expand All @@ -206,45 +224,29 @@ class FooStr(str):
def __float__(self):
return float(str(self)) + 1

self.assertEqual(float(Foo1()), 42.)
self.assertEqual(float(FloatLike(42.)), 42.)
self.assertEqual(float(Foo2()), 42.)
with self.assertWarns(DeprecationWarning):
self.assertEqual(float(Foo3(21)), 42.)
self.assertRaises(TypeError, float, Foo4(42))
self.assertEqual(float(FooStr('8')), 9.)

class Foo5:
def __float__(self):
return ""
self.assertRaises(TypeError, time.sleep, Foo5())
self.assertRaises(TypeError, time.sleep, FloatLike(""))

# Issue #24731
class F:
def __float__(self):
return OtherFloatSubclass(42.)
f = FloatLike(OtherFloatSubclass(42.))
with self.assertWarns(DeprecationWarning):
self.assertEqual(float(F()), 42.)
self.assertEqual(float(f), 42.)
with self.assertWarns(DeprecationWarning):
self.assertIs(type(float(F())), float)
self.assertIs(type(float(f)), float)
with self.assertWarns(DeprecationWarning):
self.assertEqual(FloatSubclass(F()), 42.)
self.assertEqual(FloatSubclass(f), 42.)
with self.assertWarns(DeprecationWarning):
self.assertIs(type(FloatSubclass(F())), FloatSubclass)

class MyIndex:
def __init__(self, value):
self.value = value
def __index__(self):
return self.value
self.assertIs(type(FloatSubclass(f)), FloatSubclass)

self.assertEqual(float(MyIndex(42)), 42.0)
self.assertRaises(OverflowError, float, MyIndex(2**2000))

class MyInt:
def __int__(self):
return 42

self.assertRaises(TypeError, float, MyInt())
self.assertRaises(TypeError, float, MyInt(42))

def test_keyword_args(self):
with self.assertRaisesRegex(TypeError, 'keyword argument'):
Expand Down Expand Up @@ -277,6 +279,37 @@ def __new__(cls, arg, newarg=None):
self.assertEqual(float(u), 2.5)
self.assertEqual(u.newarg, 3)

def assertEqualAndType(self, actual, expected_value, expected_type):
self.assertEqual(actual, expected_value)
self.assertIs(type(actual), expected_type)

def test_from_number(self, cls=float):
def eq(actual, expected):
self.assertEqual(actual, expected)
self.assertIs(type(actual), cls)

eq(cls.from_number(3.14), 3.14)
eq(cls.from_number(314), 314.0)
eq(cls.from_number(OtherFloatSubclass(3.14)), 3.14)
eq(cls.from_number(FloatLike(3.14)), 3.14)
eq(cls.from_number(MyIndex(314)), 314.0)

x = cls.from_number(NAN)
self.assertTrue(x != x)
self.assertIs(type(x), cls)
if cls is float:
self.assertIs(cls.from_number(NAN), NAN)

self.assertRaises(TypeError, cls.from_number, '3.14')
self.assertRaises(TypeError, cls.from_number, b'3.14')
self.assertRaises(TypeError, cls.from_number, 3.14j)
self.assertRaises(TypeError, cls.from_number, MyInt(314))
self.assertRaises(TypeError, cls.from_number, {})
self.assertRaises(TypeError, cls.from_number)

def test_from_number_subclass(self):
self.test_from_number(FloatSubclass)

def test_is_integer(self):
self.assertFalse((1.1).is_integer())
self.assertTrue((1.).is_integer())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add class methods :meth:`float.from_number` and :meth:`complex.from_number`.
11 changes: 10 additions & 1 deletion Objects/clinic/complexobject.c.h

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

11 changes: 10 additions & 1 deletion Objects/clinic/floatobject.c.h

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

62 changes: 46 additions & 16 deletions Objects/complexobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -757,22 +757,6 @@ complex___complex___impl(PyComplexObject *self)
}


static PyMethodDef complex_methods[] = {
COMPLEX_CONJUGATE_METHODDEF
COMPLEX___COMPLEX___METHODDEF
COMPLEX___GETNEWARGS___METHODDEF
COMPLEX___FORMAT___METHODDEF
{NULL, NULL} /* sentinel */
};

static PyMemberDef complex_members[] = {
{"real", Py_T_DOUBLE, offsetof(PyComplexObject, cval.real), Py_READONLY,
"the real part of a complex number"},
{"imag", Py_T_DOUBLE, offsetof(PyComplexObject, cval.imag), Py_READONLY,
"the imaginary part of a complex number"},
{0},
};

static PyObject *
complex_from_string_inner(const char *s, Py_ssize_t len, void *type)
{
Expand Down Expand Up @@ -1142,6 +1126,52 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
return complex_subtype_from_doubles(type, cr.real, ci.real);
}

/*[clinic input]
@classmethod
complex.from_number

number: object
/

Convert number to a complex floating-point number.
[clinic start generated code]*/

static PyObject *
complex_from_number(PyTypeObject *type, PyObject *number)
/*[clinic end generated code: output=658a7a5fb0de074d input=3f8bdd3a2bc3facd]*/
{
if (PyComplex_CheckExact(number) && type == &PyComplex_Type) {
Py_INCREF(number);
return number;
}
Py_complex cv = PyComplex_AsCComplex(number);
if (cv.real == -1.0 && PyErr_Occurred()) {
return NULL;
}
PyObject *result = PyComplex_FromCComplex(cv);
if (type != &PyComplex_Type && result != NULL) {
Py_SETREF(result, PyObject_CallOneArg((PyObject *)type, result));
}
return result;
}

static PyMethodDef complex_methods[] = {
COMPLEX_FROM_NUMBER_METHODDEF
COMPLEX_CONJUGATE_METHODDEF
COMPLEX___COMPLEX___METHODDEF
COMPLEX___GETNEWARGS___METHODDEF
COMPLEX___FORMAT___METHODDEF
{NULL, NULL} /* sentinel */
};

static PyMemberDef complex_members[] = {
{"real", Py_T_DOUBLE, offsetof(PyComplexObject, cval.real), Py_READONLY,
"the real part of a complex number"},
{"imag", Py_T_DOUBLE, offsetof(PyComplexObject, cval.imag), Py_READONLY,
"the imaginary part of a complex number"},
{0},
};

static PyNumberMethods complex_as_number = {
(binaryfunc)complex_add, /* nb_add */
(binaryfunc)complex_sub, /* nb_subtract */
Expand Down
Loading
Loading