Skip to content

gh-109218: Deprecate weird cases in the complex() constructor #119620

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 13 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -404,6 +404,10 @@ are always available. They are listed here in alphabetical order.
Falls back to :meth:`~object.__index__` if :meth:`~object.__complex__` and
:meth:`~object.__float__` are not defined.

.. deprecated:: 3.14
Passing a complex number as the *real* or *imag* argument is now
deprecated; it should only be passed as a single positional argument.


.. function:: delattr(object, name)

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 @@ -103,6 +103,10 @@ Optimizations
Deprecated
==========

* Passing a complex number as the *real* or *imag* argument in the
:func:`complex` constructor is now deprecated; it should only be passed
as a single positional argument.
(Contributed by Serhiy Storchaka in :gh:`109218`.)


Removed
Expand Down
70 changes: 49 additions & 21 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,25 +382,53 @@ def check(z, x, y):
check(complex(1.0, 10.0), 1.0, 10.0)
check(complex(4.25, 0.5), 4.25, 0.5)

check(complex(4.25+0j, 0), 4.25, 0.0)
check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0)
check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0)
check(complex(4.25j, 0), 0.0, 4.25)
check(complex(0j, 4.25), 0.0, 4.25)
check(complex(0, 4.25+0j), 0.0, 4.25)
check(complex(0, ComplexSubclass(4.25+0j)), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(4.25+0j, 0), 4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not .*ComplexSubclass"):
check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not .*WithComplex"):
check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(4.25j, 0), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(0j, 4.25), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'imag' must be a real number, not complex"):
check(complex(0, 4.25+0j), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'imag' must be a real number, not .*ComplexSubclass"):
check(complex(0, ComplexSubclass(4.25+0j)), 0.0, 4.25)
with self.assertRaisesRegex(TypeError,
"second argument must be a number, not 'WithComplex'"):
"argument 'imag' must be a real number, not .*WithComplex"):
complex(0, WithComplex(4.25+0j))
check(complex(0.0, 4.25j), -4.25, 0.0)
check(complex(4.25+0j, 0j), 4.25, 0.0)
check(complex(4.25j, 0j), 0.0, 4.25)
check(complex(0j, 4.25+0j), 0.0, 4.25)
check(complex(0j, 4.25j), -4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'imag' must be a real number, not complex"):
check(complex(0.0, 4.25j), -4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(4.25+0j, 0j), 4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(4.25j, 0j), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(0j, 4.25+0j), 0.0, 4.25)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(0j, 4.25j), -4.25, 0.0)

check(complex(real=4.25), 4.25, 0.0)
check(complex(real=4.25+0j), 4.25, 0.0)
check(complex(real=4.25+1.5j), 4.25, 1.5)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(real=4.25+0j), 4.25, 0.0)
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
check(complex(real=4.25+1.5j), 4.25, 1.5)
check(complex(imag=1.5), 0.0, 1.5)
check(complex(real=4.25, imag=1.5), 4.25, 1.5)
check(complex(4.25, imag=1.5), 4.25, 1.5)
Expand All @@ -420,22 +448,22 @@ def check(z, x, y):
del c, c2

self.assertRaisesRegex(TypeError,
"first argument must be a string or a number, not 'dict'",
"argument must be a string or a number, not dict",
complex, {})
self.assertRaisesRegex(TypeError,
"first argument must be a string or a number, not 'NoneType'",
"argument must be a string or a number, not NoneType",
complex, None)
self.assertRaisesRegex(TypeError,
"first argument must be a string or a number, not 'dict'",
"argument 'real' must be a real number, not dict",
complex, {1:2}, 0)
self.assertRaisesRegex(TypeError,
"can't take second arg if first is a string",
"argument 'real' must be a real number, not str",
complex, '1', 0)
self.assertRaisesRegex(TypeError,
"second argument must be a number, not 'dict'",
"argument 'imag' must be a real number, not dict",
complex, 0, {1:2})
self.assertRaisesRegex(TypeError,
"second arg can't be a string",
"argument 'imag' must be a real number, not str",
complex, 0, '1')

self.assertRaises(TypeError, complex, WithComplex(1.5))
Expand Down
5 changes: 4 additions & 1 deletion Lib/test/test_fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,10 @@ def testMixedMultiplication(self):
self.assertTypedEquals(F(3, 2) * Polar(4, 2), Polar(F(6, 1), 2))
self.assertTypedEquals(F(3, 2) * Polar(4.0, 2), Polar(6.0, 2))
self.assertTypedEquals(F(3, 2) * Rect(4, 3), Rect(F(6, 1), F(9, 2)))
self.assertTypedEquals(F(3, 2) * RectComplex(4, 3), RectComplex(6.0+0j, 4.5+0j))
with self.assertWarnsRegex(DeprecationWarning,
"argument 'real' must be a real number, not complex"):
self.assertTypedEquals(F(3, 2) * RectComplex(4, 3),
RectComplex(6.0+0j, 4.5+0j))
self.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2))
self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j)
self.assertEqual(F(3, 2) * SymbolicComplex('X'), SymbolicComplex('3/2 * X'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`complex` accepts now a string only as a positional argument. Passing
a complex number as the "real" or "imag" argument is deprecated; it should
only be passed as a single positional argument.
139 changes: 100 additions & 39 deletions Objects/complexobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -894,8 +894,8 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
}
else {
PyErr_Format(PyExc_TypeError,
"complex() argument must be a string or a number, not '%.200s'",
Py_TYPE(v)->tp_name);
"complex() argument must be a string or a number, not %T",
v);
return NULL;
}

Expand All @@ -905,6 +905,77 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
return result;
}

/* The constructor should only accept a string as a positional argument,
* not as by the 'real' keyword. But Argument Clinic does not allow
* to distinguish between argument passed positionally and by keyword.
* So the constructor must be split into two parts: actual_complex_new()
* handles the case of no arguments and one positional argument, and calls
* complex_new(), implemented with Argument Clinic, to handle the remaining
* cases: 'real' and 'imag' arguments. This separation is well suited
* for different constructor roles: convering a string or number to a complex
* number and constructing a complex number from real and imaginary parts.
*/
static PyObject *
Copy link
Member

Choose a reason for hiding this comment

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

Would it be worth adding a comment above this function explaining the purpose (and explaining why this is different from complex_new)?

actual_complex_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
PyObject *res = NULL;
PyNumberMethods *nbr;

if (PyTuple_GET_SIZE(args) > 1 || (kwargs != NULL && PyDict_GET_SIZE(kwargs))) {
return complex_new(type, args, kwargs);
}
if (!PyTuple_GET_SIZE(args)) {
return complex_subtype_from_doubles(type, 0, 0);
}

PyObject *arg = PyTuple_GET_ITEM(args, 0);
/* Special-case for a single argument when type(arg) is complex. */
if (PyComplex_CheckExact(arg) && type == &PyComplex_Type) {
/* Note that we can't know whether it's safe to return
a complex *subclass* instance as-is, hence the restriction
to exact complexes here. If either the input or the
output is a complex subclass, it will be handled below
as a non-orthogonal vector. */
return Py_NewRef(arg);
}
if (PyUnicode_Check(arg)) {
return complex_subtype_from_string(type, arg);
}
PyObject *tmp = try_complex_special_method(arg);
if (tmp) {
Py_complex c = ((PyComplexObject*)tmp)->cval;
res = complex_subtype_from_doubles(type, c.real, c.imag);
Py_DECREF(tmp);
}
else if (PyErr_Occurred()) {
return NULL;
}
else if (PyComplex_Check(arg)) {
Copy link
Member

Choose a reason for hiding this comment

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

FYI, this is not tested. But I'm not sure if that's a right logic. Complex subclasses should be covered by try_complex_special_method(), c.f. PyNumber_Float().

Copy link
Member Author

Choose a reason for hiding this comment

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

try_complex_special_method is relatively expensive in comparison to PyNumber_Float(), because there is no nb_complex slot. And __complex__ is looked up even for exact complex, float and int. I planned to do something with this, but in a different PR. We can not also completely exclude complex subclasses without __complex__.

Copy link
Member

Choose a reason for hiding this comment

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

try_complex_special_method is relatively expensive in comparison to PyNumber_Float()

That seems to be an implementation detail. I wonder if we could add nb_complex slot: there is a reserved slot right now anyway.

And complex is looked up even for exact complex, float and int.

The current (i.e. in the main) code - uses here same logic as the float constructor: there is a case for exact complex (as for exact float in PyNumber_Float()).

We can not also completely exclude complex subclasses without complex.

People could break thing in very crazy ways, but should we support such cases? (Looks as a variant of #112636.)

Copy link
Member

Choose a reason for hiding this comment

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

/* Note that if arg is of a complex subtype, we're only
retaining its real & imag parts here, and the return
value is (properly) of the builtin complex type. */
Py_complex c = ((PyComplexObject*)arg)->cval;
res = complex_subtype_from_doubles(type, c.real, c.imag);
}
Comment on lines +953 to +959
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
else if (PyComplex_Check(arg)) {
/* Note that if arg is of a complex subtype, we're only
retaining its real & imag parts here, and the return
value is (properly) of the builtin complex type. */
Py_complex c = ((PyComplexObject*)arg)->cval;
res = complex_subtype_from_doubles(type, c.real, c.imag);
}

That seems redundant (and untested). Complex subclasses have __complex__() dunder, so they should be handled by try_complex_special_method() helper (even if the dunder is broken somehow).

else if ((nbr = Py_TYPE(arg)->tp_as_number) != NULL &&
Copy link
Member

Choose a reason for hiding this comment

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

One branch missed:

     952         [ +  - ]:         55 :     else if ((nbr = Py_TYPE(arg)->tp_as_number) != NULL &&
     953   [ +  +  +  + ]:         55 :              (nbr->nb_float != NULL || nbr->nb_index != NULL))

Copy link
Member Author

Choose a reason for hiding this comment

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

How to interpret this? It looks to me that all possible combinations are covered.

  • nbr == NULL: complex({})
  • nbr != NULL && nbr->nb_float != NULL: complex(MockFloat(4.25))
  • nbr != NULL && nbr->nb_float == NULL && nbr->nb_index != NULL: complex(MockIndex(42))
  • nbr != NULL && nbr->nb_float == NULL && nbr->nb_index == NULL: complex(MockInt())

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, branch coverage output for C code looks cryptic. Try complex([]), i.e. nbr != NULL, but it has no nb_float/index, that works for me.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think complex(MyInt()) (where MyInt has the __int__ method already covers this).

Copy link
Member

Choose a reason for hiding this comment

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

No. Oh, I see: that should be nbr == NULL condition. So complex(object()) will work too.

(nbr->nb_float != NULL || nbr->nb_index != NULL))
{
/* The argument really is entirely real, and contributes
nothing in the imaginary direction.
Just treat it as a double. */
double r = PyFloat_AsDouble(arg);
if (r != -1.0 || !PyErr_Occurred()) {
res = complex_subtype_from_doubles(type, r, 0);
}
}
else {
PyErr_Format(PyExc_TypeError,
"complex() argument must be a string or a number, not %T",
arg);
}
return res;
}

/*[clinic input]
@classmethod
complex.__new__ as complex_new
Expand All @@ -930,32 +1001,10 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
if (r == NULL) {
r = _PyLong_GetZero();
Copy link
Member

Choose a reason for hiding this comment

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

This inaccessible.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is now covered by a new test added in #119635 for complex(imag=1.5).

}
PyObject *orig_r = r;

/* Special-case for a single argument when type(arg) is complex. */
if (PyComplex_CheckExact(r) && i == NULL &&
type == &PyComplex_Type) {
/* Note that we can't know whether it's safe to return
a complex *subclass* instance as-is, hence the restriction
to exact complexes here. If either the input or the
output is a complex subclass, it will be handled below
as a non-orthogonal vector. */
return Py_NewRef(r);
}
if (PyUnicode_Check(r)) {
if (i != NULL) {
PyErr_SetString(PyExc_TypeError,
"complex() can't take second arg"
" if first is a string");
return NULL;
}
return complex_subtype_from_string(type, r);
}
if (i != NULL && PyUnicode_Check(i)) {
PyErr_SetString(PyExc_TypeError,
"complex() second arg can't be a string");
return NULL;
}

/* DEPRECATED: The call of try_complex_special_method() for the "real"
* part will be dropped after the end of the deprecation period. */
tmp = try_complex_special_method(r);
Copy link
Member

Choose a reason for hiding this comment

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

IIUIC, this logic will be dropped after a deprecation period as well. I'm not sure if this is obvious, maybe worth a comment.

if (tmp) {
Copy link
Member

Choose a reason for hiding this comment

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

else if (PyErr_Occurred()) branch is not tested.

r = tmp;
Expand All @@ -970,9 +1019,8 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
(nbr->nb_float == NULL && nbr->nb_index == NULL && !PyComplex_Check(r)))
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, it seems there is a coverage regression, not all branches are tested.

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 should be covered by new tests added in test added in #119635.

Copy link
Member

Choose a reason for hiding this comment

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

Now it's better, yet one branch seems to be missed:

   1029         [ +  - ]:       3869 :     if (nbr == NULL ||

Looks like it's nbr!=NULL.

{
PyErr_Format(PyExc_TypeError,
"complex() first argument must be a string or a number, "
"not '%.200s'",
Py_TYPE(r)->tp_name);
"complex() argument 'real' must be a real number, not %T",
r);
if (own_r) {
Copy link
Member

@skirpichev skirpichev May 28, 2024

Choose a reason for hiding this comment

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

Actually, own_r condition here is inaccessible. If we are here - try_complex_special_method() call above was unsuccessful.

See #109642. Maybe it worth to include constructor-related changes from that pr.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll see.

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 code will be removed after the end of the deprecation period. So it is not worth to spent effort to optimize it.

Py_DECREF(r);
}
Expand All @@ -984,9 +1032,8 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
(nbi->nb_float == NULL && nbi->nb_index == NULL && !PyComplex_Check(i)))
{
PyErr_Format(PyExc_TypeError,
"complex() second argument must be a number, "
"not '%.200s'",
Py_TYPE(i)->tp_name);
"complex() argument 'imag' must be a real number, not %T",
i);
if (own_r) {
Py_DECREF(r);
}
Expand All @@ -998,6 +1045,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
both be treated as numbers, and the constructor should return a
complex number equal to (real + imag*1j).

The following is DEPRECATED:
Note that we do NOT assume the input to already be in canonical
form; the "real" and "imag" parts might themselves be complex
numbers, which slightly complicates the code below. */
Expand All @@ -1008,19 +1056,27 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
cr = ((PyComplexObject*)r)->cval;
cr_is_complex = 1;
if (own_r) {
/* r was a newly created complex number, rather
than the original "real" argument. */
Py_DECREF(r);
}
nbr = Py_TYPE(orig_r)->tp_as_number;
if (nbr == NULL ||
(nbr->nb_float == NULL && nbr->nb_index == NULL))
Comment on lines +1064 to +1065
Copy link
Member

Choose a reason for hiding this comment

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

This also looks untested:

    1072         [ +  - ]:         10 :         if (nbr == NULL ||
    1073   [ +  -  +  - ]:         10 :             (nbr->nb_float == NULL && nbr->nb_index == NULL))

{
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"complex() argument 'real' must be a real number, not %T",
orig_r)) {
return NULL;
}
}
}
else {
/* The "real" part really is entirely real, and contributes
nothing in the imaginary direction.
Just treat it as a double. */
tmp = PyNumber_Float(r);
if (own_r) {
/* r was a newly created complex number, rather
than the original "real" argument. */
Py_DECREF(r);
}
assert(!own_r);
if (tmp == NULL)
return NULL;
assert(PyFloat_Check(tmp));
Expand All @@ -1032,6 +1088,11 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
ci.real = cr.imag;
}
else if (PyComplex_Check(i)) {
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"complex() argument 'imag' must be a real number, not %T",
i)) {
return NULL;
}
ci = ((PyComplexObject*)i)->cval;
ci_is_complex = 1;
} else {
Expand Down Expand Up @@ -1131,6 +1192,6 @@ PyTypeObject PyComplex_Type = {
0, /* tp_dictoffset */
0, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
complex_new, /* tp_new */
actual_complex_new, /* tp_new */
PyObject_Del, /* tp_free */
};
Loading