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
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ are always available. They are listed here in alphabetical order.
to :meth:`~object.__float__`. If :meth:`!__float__` is not defined then it falls back
to :meth:`~object.__index__`.

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

.. note::

When converting from a string, the string must not contain whitespace
Expand Down Expand Up @@ -684,6 +686,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 accept numeric argument.

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

Examples::
Expand Down
36 changes: 36 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,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.13


.. method:: float.as_integer_ratio()

Return a pair of integers whose ratio is exactly equal to the
Expand Down Expand Up @@ -702,6 +719,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.13


.. _numeric-hash:

Hashing of numeric types
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ 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 error if the argument is a string.
(Contributed by Serhiy Storchaka in :gh:`84978`.)

* Allow the *count* argument of :meth:`str.replace` to be a keyword.
(Contributed by Hugo van Kemenade in :gh:`106487`.)

Expand Down
117 changes: 80 additions & 37 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@

INF = float("inf")
NAN = float("nan")

class ComplexSubclass(complex):
pass

class OtherComplexSubclass(complex):
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 ComplexLike:
def __init__(self, value):
self.value = value

def __complex__(self):
return self.value


# These tests ensure that complex math does the right thing

ZERO_DIVISION = (
Expand Down Expand Up @@ -306,14 +342,11 @@ def test_conjugate(self):
self.assertClose(complex(5.3, 9.8).conjugate(), 5.3-9.8j)

def test_constructor(self):
class NS:
def __init__(self, value): self.value = value
def __complex__(self): return self.value
self.assertEqual(complex(NS(1+10j)), 1+10j)
self.assertRaises(TypeError, complex, NS(None))
self.assertEqual(complex(ComplexLike(1+10j)), 1+10j)
self.assertRaises(TypeError, complex, ComplexLike(None))
self.assertRaises(TypeError, complex, {})
self.assertRaises(TypeError, complex, NS(1.5))
self.assertRaises(TypeError, complex, NS(1))
self.assertRaises(TypeError, complex, ComplexLike(1.5))
self.assertRaises(TypeError, complex, ComplexLike(1))

self.assertAlmostEqual(complex("1+10j"), 1+10j)
self.assertAlmostEqual(complex(10), 10+0j)
Expand Down Expand Up @@ -360,8 +393,7 @@ def __complex__(self): return self.value
self.assertAlmostEqual(complex('-1e-500j'), 0.0 - 0.0j)
self.assertAlmostEqual(complex('-1e-500+1e-500j'), -0.0 + 0.0j)

class complex2(complex): pass
self.assertAlmostEqual(complex(complex2(1+1j)), 1+1j)
self.assertAlmostEqual(complex(ComplexSubclass(1+1j)), 1+1j)
self.assertAlmostEqual(complex(real=17, imag=23), 17+23j)
self.assertAlmostEqual(complex(real=17+23j), 17+23j)
self.assertAlmostEqual(complex(real=17+23j, imag=23), 17+46j)
Expand Down Expand Up @@ -443,33 +475,17 @@ def __complex__(self):

self.assertRaises(EvilExc, complex, evilcomplex())

class float2:
def __init__(self, value):
self.value = value
def __float__(self):
return self.value

self.assertAlmostEqual(complex(float2(42.)), 42)
self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j)
self.assertRaises(TypeError, complex, float2(None))

class MyIndex:
def __init__(self, value):
self.value = value
def __index__(self):
return self.value
self.assertAlmostEqual(complex(FloatLike(42.)), 42)
self.assertAlmostEqual(complex(real=FloatLike(17.), imag=FloatLike(23.)), 17+23j)
self.assertRaises(TypeError, complex, FloatLike(None))

self.assertAlmostEqual(complex(MyIndex(42)), 42.0+0.0j)
self.assertAlmostEqual(complex(123, MyIndex(42)), 123.0+42.0j)
self.assertRaises(OverflowError, complex, MyIndex(2**2000))
self.assertRaises(OverflowError, complex, 123, MyIndex(2**2000))

class MyInt:
def __int__(self):
return 42

self.assertRaises(TypeError, complex, MyInt())
self.assertRaises(TypeError, complex, 123, MyInt())
self.assertRaises(TypeError, complex, MyInt(42))
self.assertRaises(TypeError, complex, 123, MyInt(42))

class complex0(complex):
"""Test usage of __complex__() when inheriting from 'complex'"""
Expand Down Expand Up @@ -508,24 +524,22 @@ class complex_subclass(complex):

@support.requires_IEEE_754
def test_constructor_special_numbers(self):
class complex2(complex):
pass
for x in 0.0, -0.0, INF, -INF, NAN:
for y in 0.0, -0.0, INF, -INF, NAN:
with self.subTest(x=x, y=y):
z = complex(x, y)
self.assertFloatsAreIdentical(z.real, x)
self.assertFloatsAreIdentical(z.imag, y)
z = complex2(x, y)
self.assertIs(type(z), complex2)
z = ComplexSubclass(x, y)
self.assertIs(type(z), ComplexSubclass)
self.assertFloatsAreIdentical(z.real, x)
self.assertFloatsAreIdentical(z.imag, y)
z = complex(complex2(x, y))
z = complex(ComplexSubclass(x, y))
self.assertIs(type(z), complex)
self.assertFloatsAreIdentical(z.real, x)
self.assertFloatsAreIdentical(z.imag, y)
z = complex2(complex(x, y))
self.assertIs(type(z), complex2)
z = ComplexSubclass(complex(x, y))
self.assertIs(type(z), ComplexSubclass)
self.assertFloatsAreIdentical(z.real, x)
self.assertFloatsAreIdentical(z.imag, y)

Expand All @@ -547,6 +561,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(ComplexLike(3.14+2.72j)), 3.14+2.72j)
eq(cls.from_number(FloatLike(3.14)), 3.14+0j)
eq(cls.from_number(MyIndex(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 @@ -33,6 +33,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 @@ -182,10 +204,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 @@ -207,45 +225,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 @@ -278,6 +280,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`.
Loading