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

They are alternate constructors which only accept numbers
(including objects with special methods __float__, __complex__
and __index__), but not strings.
This commit is contained in:
Serhiy Storchaka 2024-07-15 19:07:00 +03:00 committed by GitHub
parent 8303d32ff5
commit 94bee45dee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 242 additions and 46 deletions

View File

@ -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`.
@ -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`.

View File

@ -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*.
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
@ -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

View File

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

View File

@ -36,6 +36,16 @@ class WithFloat:
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
@ -675,6 +685,35 @@ class ComplexTest(unittest.TestCase):
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)))

View File

@ -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):
@ -181,10 +203,6 @@ class GeneralFloatCases(unittest.TestCase):
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.
@ -206,45 +224,29 @@ class GeneralFloatCases(unittest.TestCase):
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'):
@ -277,6 +279,37 @@ class GeneralFloatCases(unittest.TestCase):
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())

View File

@ -0,0 +1 @@
Add class methods :meth:`float.from_number` and :meth:`complex.from_number`.

View File

@ -160,4 +160,13 @@ skip_optional_pos:
exit:
return return_value;
}
/*[clinic end generated code: output=295ecfd71389d7fe input=a9049054013a1b77]*/
PyDoc_STRVAR(complex_from_number__doc__,
"from_number($type, number, /)\n"
"--\n"
"\n"
"Convert number to a complex floating-point number.");
#define COMPLEX_FROM_NUMBER_METHODDEF \
{"from_number", (PyCFunction)complex_from_number, METH_O|METH_CLASS, complex_from_number__doc__},
/*[clinic end generated code: output=188438cc9ae167f7 input=a9049054013a1b77]*/

View File

@ -227,6 +227,15 @@ exit:
return return_value;
}
PyDoc_STRVAR(float_from_number__doc__,
"from_number($type, number, /)\n"
"--\n"
"\n"
"Convert real number to a floating-point number.");
#define FLOAT_FROM_NUMBER_METHODDEF \
{"from_number", (PyCFunction)float_from_number, METH_O|METH_CLASS, float_from_number__doc__},
PyDoc_STRVAR(float___getnewargs____doc__,
"__getnewargs__($self, /)\n"
"--\n"
@ -318,4 +327,4 @@ float___format__(PyObject *self, PyObject *arg)
exit:
return return_value;
}
/*[clinic end generated code: output=c79743c8551c30d9 input=a9049054013a1b77]*/
/*[clinic end generated code: output=b9c8a1b6759ca073 input=a9049054013a1b77]*/

View File

@ -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)
{
@ -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 */

View File

@ -1669,6 +1669,36 @@ float_vectorcall(PyObject *type, PyObject * const*args,
}
/*[clinic input]
@classmethod
float.from_number
number: object
/
Convert real number to a floating-point number.
[clinic start generated code]*/
static PyObject *
float_from_number(PyTypeObject *type, PyObject *number)
/*[clinic end generated code: output=bbcf05529fe907a3 input=1f8424d9bc11866a]*/
{
if (PyFloat_CheckExact(number) && type == &PyFloat_Type) {
Py_INCREF(number);
return number;
}
double x = PyFloat_AsDouble(number);
if (x == -1.0 && PyErr_Occurred()) {
return NULL;
}
PyObject *result = PyFloat_FromDouble(x);
if (type != &PyFloat_Type && result != NULL) {
Py_SETREF(result, PyObject_CallOneArg((PyObject *)type, result));
}
return result;
}
/*[clinic input]
float.__getnewargs__
[clinic start generated code]*/
@ -1782,6 +1812,7 @@ float___format___impl(PyObject *self, PyObject *format_spec)
}
static PyMethodDef float_methods[] = {
FLOAT_FROM_NUMBER_METHODDEF
FLOAT_CONJUGATE_METHODDEF
FLOAT___TRUNC___METHODDEF
FLOAT___FLOOR___METHODDEF