]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-146151: Add support for complex arrays in the array module (#146237)
authorSergey B Kirpichev <skirpichev@gmail.com>
Thu, 26 Mar 2026 14:45:20 +0000 (17:45 +0300)
committerGitHub <noreply@github.com>
Thu, 26 Mar 2026 14:45:20 +0000 (15:45 +0100)
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/library/array.rst
Doc/whatsnew/3.15.rst
Lib/test/test_array.py
Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst [new file with mode: 0644]
Modules/arraymodule.c
Modules/clinic/arraymodule.c.h

index 783b98913653df069eac01321a70c43c20acd0a8..e8d7fa8e39a410250d653c2b8b645aeddb01adf0 100644 (file)
@@ -9,7 +9,7 @@
 --------------
 
 This module defines an object type which can compactly represent an array of
-basic values: characters, integers, floating-point numbers.  Arrays are mutable :term:`sequence`
+basic values: characters, integers, floating-point numbers, complex numbers.  Arrays are mutable :term:`sequence`
 types and behave very much like lists, except that the type of objects stored in
 them is constrained.  The type is specified at object creation time by using a
 :dfn:`type code`, which is a single character.  The following type codes are
@@ -46,6 +46,11 @@ defined:
 +-----------+--------------------+-------------------+-----------------------+-------+
 | ``'d'``   | double             | float             | 8                     |       |
 +-----------+--------------------+-------------------+-----------------------+-------+
+| ``'F'``   | float complex      | complex           | 8                     | \(3)  |
++-----------+--------------------+-------------------+-----------------------+-------+
+| ``'D'``   | double complex     | complex           | 16                    | \(3)  |
++-----------+--------------------+-------------------+-----------------------+-------+
+
 
 Notes:
 
@@ -63,6 +68,15 @@ Notes:
 (2)
    .. versionadded:: 3.13
 
+(3)
+   Complex types (``F`` and ``D``) are available unconditionally,
+   regardless on support for complex types (the Annex G of the C11 standard)
+   by the C compiler.
+   As specified in the C11 standard, each complex type is represented by a
+   two-element C array containing, respectively, the real and imaginary parts.
+
+   .. versionadded:: 3.15
+
 .. seealso::
 
    The :ref:`ctypes <ctypes-fundamental-data-types>` and
@@ -146,9 +160,10 @@ The module defines the following type:
    .. method:: byteswap()
 
       "Byteswap" all items of the array.  This is only supported for values which are
-      1, 2, 4, or 8 bytes in size; for other types of values, :exc:`RuntimeError` is
+      1, 2, 4, 8 or 16 bytes in size; for other types of values, :exc:`RuntimeError` is
       raised.  It is useful when reading data from a file written on a machine with a
-      different byte order.
+      different byte order.  Note, that for complex types the order of
+      components (the real part, followed by imaginary part) is preserved.
 
 
    .. method:: count(x)
index 57b72cf548eee99b087090f04516cabeaa7604d7..76e97cf4b555957fda3a36c0d20ae650c438bb1e 100644 (file)
@@ -634,6 +634,14 @@ argparse
   (Contributed by Savannah Ostrowski in :gh:`142390`.)
 
 
+array
+-----
+
+* Support the :c:expr:`float complex` and :c:expr:`double complex` C types:
+  formatting characters ``'F'`` and ``'D'`` respectively.
+  (Contributed by Sergey B Kirpichev in :gh:`146151`.)
+
+
 base64
 ------
 
index 5c919aea24ed94584416c2edb4a9f7d40915dfc2..b3561e7650a0ad65f39f6fd4ecf0d0364ec98a18 100755 (executable)
@@ -31,7 +31,7 @@ class ArraySubclassWithKwargs(array.array):
     def __init__(self, typecode, newarg=None):
         array.array.__init__(self)
 
-typecodes = 'uwbBhHiIlLfdqQ'
+typecodes = 'uwbBhHiIlLfdqQFD'
 
 class MiscTest(unittest.TestCase):
 
@@ -113,6 +113,12 @@ UTF16_LE = 18
 UTF16_BE = 19
 UTF32_LE = 20
 UTF32_BE = 21
+IEEE_754_FLOAT_COMPLEX_LE = 22
+IEEE_754_FLOAT_COMPLEX_BE = 23
+IEEE_754_DOUBLE_COMPLEX_LE = 24
+IEEE_754_DOUBLE_COMPLEX_BE = 25
+
+MACHINE_FORMAT_CODE_MAX = 25
 
 
 class ArrayReconstructorTest(unittest.TestCase):
@@ -139,7 +145,7 @@ class ArrayReconstructorTest(unittest.TestCase):
         self.assertRaises(ValueError, array_reconstructor,
                           array.array, "b", UNKNOWN_FORMAT, b"")
         self.assertRaises(ValueError, array_reconstructor,
-                          array.array, "b", 22, b"")
+                          array.array, "b", MACHINE_FORMAT_CODE_MAX + 1, b"")
         self.assertRaises(ValueError, array_reconstructor,
                           array.array, "d", 16, b"a")
 
@@ -191,7 +197,15 @@ class ArrayReconstructorTest(unittest.TestCase):
             (['d'], IEEE_754_DOUBLE_LE, '<dddd',
              [9006104071832581.0, float('inf'), float('-inf'), -0.0]),
             (['d'], IEEE_754_DOUBLE_BE, '>dddd',
-             [9006104071832581.0, float('inf'), float('-inf'), -0.0])
+             [9006104071832581.0, float('inf'), float('-inf'), -0.0]),
+            (['F'], IEEE_754_FLOAT_COMPLEX_LE, '<FFFF',
+             [16711938.0j, float('inf'), complex('1-infj'), -0.0]),
+            (['F'], IEEE_754_FLOAT_COMPLEX_BE, '>FFFF',
+             [16711938.0j, float('inf'), complex('1-infj'), -0.0]),
+            (['D'], IEEE_754_DOUBLE_COMPLEX_LE, '<DDDD',
+             [9006104071832581.0j, float('inf'), complex('1-infj'), -0.0]),
+            (['D'], IEEE_754_DOUBLE_COMPLEX_BE, '>DDDD',
+             [9006104071832581.0j, float('inf'), complex('1-infj'), -0.0]),
         )
         for testcase in testcases:
             valid_typecodes, mformat_code, struct_fmt, values = testcase
@@ -279,7 +293,7 @@ class BaseTest:
             example = self.example
         a = array.array(self.typecode, example)
         self.assertRaises(TypeError, a.byteswap, 42)
-        if a.itemsize in (1, 2, 4, 8):
+        if a.itemsize in (1, 2, 4, 8, 16):
             b = array.array(self.typecode, example)
             b.byteswap()
             if a.itemsize==1:
@@ -1525,6 +1539,55 @@ class FPTest(NumberTest):
             b.byteswap()
             self.assertEqual(a, b)
 
+class CFPTest(NumberTest):
+    example = [-42j, 0, 42+1j, 1e5j, -1e10]
+    outside = 23
+
+    def assertEntryEqual(self, entry1, entry2):
+        self.assertAlmostEqual(entry1, entry2)
+
+    def test_cmp(self):
+        a = array.array(self.typecode, self.example)
+        self.assertIs(a == 42, False)
+        self.assertIs(a != 42, True)
+
+        self.assertIs(a == a, True)
+        self.assertIs(a != a, False)
+        self.assertIs(a < a, False)
+        self.assertIs(a <= a, True)
+        self.assertIs(a > a, False)
+        self.assertIs(a >= a, True)
+
+        self.assertIs(a == 2*a, False)
+        self.assertIs(a != 2*a, True)
+        self.assertIs(a < 2*a, True)
+        self.assertIs(a <= 2*a, True)
+        self.assertIs(a > 2*a, False)
+        self.assertIs(a >= 2*a, False)
+
+    def test_nan(self):
+        a = array.array(self.typecode, [float('nan')])
+        b = array.array(self.typecode, [float('nan')])
+        self.assertIs(a != b, True)
+        self.assertIs(a == b, False)
+
+    def test_byteswap(self):
+        a = array.array(self.typecode, self.example)
+        self.assertRaises(TypeError, a.byteswap, 42)
+        if a.itemsize in (1, 2, 4, 8):
+            b = array.array(self.typecode, self.example)
+            b.byteswap()
+            if a.itemsize == 1:
+                self.assertEqual(a, b)
+            else:
+                # On alphas treating the byte swapped bit patterns as
+                # floats/doubles results in floating-point exceptions
+                # => compare the 8bit string values instead
+                self.assertNotEqual(a.tobytes(), b.tobytes())
+            b.byteswap()
+            self.assertEqual(a, b)
+
+
 class FloatTest(FPTest, unittest.TestCase):
     typecode = 'f'
     minitemsize = 4
@@ -1551,6 +1614,15 @@ class DoubleTest(FPTest, unittest.TestCase):
             self.fail("Array of size > maxsize created - MemoryError expected")
 
 
+class ComplexFloatTest(CFPTest, unittest.TestCase):
+    typecode = 'F'
+    minitemsize = 8
+
+class ComplexDoubleTest(CFPTest, unittest.TestCase):
+    typecode = 'D'
+    minitemsize = 16
+
+
 class LargeArrayTest(unittest.TestCase):
     typecode = 'b'
 
diff --git a/Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst b/Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst
new file mode 100644 (file)
index 0000000..020b7d7
--- /dev/null
@@ -0,0 +1,3 @@
+Support the :c:expr:`float complex` and :c:expr:`double complex` C types
+in the :mod:`array` module: formatting characters ``'F'`` and ``'D'``
+respectively.  Patch by Sergey B Kirpichev.
index f4a888af16f942ac807df6233a356be632cd2ea0..42572833c2128ae746fec9e3e783fb25d0a6e99b 100644 (file)
@@ -115,10 +115,14 @@ enum machine_format_code {
     UTF16_LE = 18,
     UTF16_BE = 19,
     UTF32_LE = 20,
-    UTF32_BE = 21
+    UTF32_BE = 21,
+    IEEE_754_FLOAT_COMPLEX_LE = 22,
+    IEEE_754_FLOAT_COMPLEX_BE = 23,
+    IEEE_754_DOUBLE_COMPLEX_LE = 24,
+    IEEE_754_DOUBLE_COMPLEX_BE = 25
 };
 #define MACHINE_FORMAT_CODE_MIN 0
-#define MACHINE_FORMAT_CODE_MAX 21
+#define MACHINE_FORMAT_CODE_MAX 25
 
 
 /*
@@ -647,6 +651,64 @@ d_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
     return 0;
 }
 
+static PyObject *
+cf_getitem(arrayobject *ap, Py_ssize_t i)
+{
+    float f[2];
+
+    memcpy(&f, ap->ob_item + i*sizeof(f), sizeof(f));
+    return PyComplex_FromDoubles(f[0], f[1]);
+}
+
+static int
+cf_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
+{
+    Py_complex x;
+    float f[2];
+
+    if (!PyArg_Parse(v, "D;array item must be complex", &x)) {
+        return -1;
+    }
+
+    CHECK_ARRAY_BOUNDS(ap, i);
+
+    f[0] = (float)x.real;
+    f[1] = (float)x.imag;
+    if (i >= 0) {
+        memcpy(ap->ob_item + i*sizeof(f), &f, sizeof(f));
+    }
+    return 0;
+}
+
+static PyObject *
+cd_getitem(arrayobject *ap, Py_ssize_t i)
+{
+    double f[2];
+
+    memcpy(&f, ap->ob_item + i*sizeof(f), sizeof(f));
+    return PyComplex_FromDoubles(f[0], f[1]);
+}
+
+static int
+cd_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
+{
+    Py_complex x;
+    double f[2];
+
+    if (!PyArg_Parse(v, "D;array item must be complex", &x)) {
+        return -1;
+    }
+
+    CHECK_ARRAY_BOUNDS(ap, i);
+
+    f[0] = x.real;
+    f[1] = x.imag;
+    if (i >= 0) {
+        memcpy(ap->ob_item + i*sizeof(f), &f, sizeof(f));
+    }
+    return 0;
+}
+
 #define DEFINE_COMPAREITEMS(code, type) \
     static int \
     code##_compareitems(const void *lhs, const void *rhs, Py_ssize_t length) \
@@ -691,6 +753,8 @@ static const struct arraydescr descriptors[] = {
     {'Q', sizeof(long long), QQ_getitem, QQ_setitem, QQ_compareitems, "Q", 1, 0},
     {'f', sizeof(float), f_getitem, f_setitem, NULL, "f", 0, 0},
     {'d', sizeof(double), d_getitem, d_setitem, NULL, "d", 0, 0},
+    {'F', 2*sizeof(float), cf_getitem, cf_setitem, NULL, "F", 0, 0},
+    {'D', 2*sizeof(double), cd_getitem, cd_setitem, NULL, "D", 0, 0},
     {'\0', 0, 0, 0, 0, 0, 0} /* Sentinel */
 };
 
@@ -1494,13 +1558,14 @@ array.array.byteswap
 
 Byteswap all items of the array.
 
-If the items in the array are not 1, 2, 4, or 8 bytes in size, RuntimeError is
-raised.
+If the items in the array are not 1, 2, 4, 8 or 16 bytes in size, RuntimeError
+is raised.  Note, that for complex types the order of
+components (the real part, followed by imaginary part) is preserved.
 [clinic start generated code]*/
 
 static PyObject *
 array_array_byteswap_impl(arrayobject *self)
-/*[clinic end generated code: output=5f8236cbdf0d90b5 input=9af1d1749000b14f]*/
+/*[clinic end generated code: output=5f8236cbdf0d90b5 input=aafda275f48191d0]*/
 {
     char *p;
     Py_ssize_t i;
@@ -1526,19 +1591,66 @@ array_array_byteswap_impl(arrayobject *self)
         }
         break;
     case 8:
+        if (self->ob_descr->typecode != 'F') {
+            for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) {
+                char p0 = p[0];
+                char p1 = p[1];
+                char p2 = p[2];
+                char p3 = p[3];
+                p[0] = p[7];
+                p[1] = p[6];
+                p[2] = p[5];
+                p[3] = p[4];
+                p[4] = p3;
+                p[5] = p2;
+                p[6] = p1;
+                p[7] = p0;
+            }
+        }
+        else {
+            for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) {
+                char t0 = p[0];
+                char t1 = p[1];
+                p[0] = p[3];
+                p[1] = p[2];
+                p[2] = t1;
+                p[3] = t0;
+                t0 = p[4];
+                t1 = p[5];
+                p[4] = p[7];
+                p[5] = p[6];
+                p[6] = t1;
+                p[7] = t0;
+            }
+        }
+        break;
+    case 16:
+        assert(self->ob_descr->typecode == 'D');
         for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) {
-            char p0 = p[0];
-            char p1 = p[1];
-            char p2 = p[2];
-            char p3 = p[3];
+            char t0 = p[0];
+            char t1 = p[1];
+            char t2 = p[2];
+            char t3 = p[3];
             p[0] = p[7];
             p[1] = p[6];
             p[2] = p[5];
             p[3] = p[4];
-            p[4] = p3;
-            p[5] = p2;
-            p[6] = p1;
-            p[7] = p0;
+            p[4] = t3;
+            p[5] = t2;
+            p[6] = t1;
+            p[7] = t0;
+            t0 = p[8];
+            t1 = p[9];
+            t2 = p[10];
+            t3 = p[11];
+            p[8] = p[15];
+            p[9] = p[14];
+            p[10] = p[13];
+            p[11] = p[12];
+            p[12] = t3;
+            p[13] = t2;
+            p[14] = t1;
+            p[15] = t0;
         }
         break;
     default:
@@ -1973,7 +2085,11 @@ static const struct mformatdescr {
     {4, 0, 0},                  /* 18: UTF16_LE */
     {4, 0, 1},                  /* 19: UTF16_BE */
     {8, 0, 0},                  /* 20: UTF32_LE */
-    {8, 0, 1}                   /* 21: UTF32_BE */
+    {8, 0, 1},                  /* 21: UTF32_BE */
+    {8, 0, 0},                  /* 22: IEEE_754_FLOAT_COMPLEX_LE */
+    {8, 0, 1},                  /* 23: IEEE_754_FLOAT_COMPLEX_BE */
+    {16, 0, 0},                 /* 24: IEEE_754_DOUBLE_COMPLEX_LE */
+    {16, 0, 1},                 /* 25: IEEE_754_DOUBLE_COMPLEX_BE */
 };
 
 
@@ -2014,6 +2130,14 @@ typecode_to_mformat_code(char typecode)
     case 'd':
         return _PY_FLOAT_BIG_ENDIAN ? IEEE_754_DOUBLE_BE : IEEE_754_DOUBLE_LE;
 
+    case 'F':
+        return _PY_FLOAT_BIG_ENDIAN ? \
+            IEEE_754_FLOAT_COMPLEX_BE : IEEE_754_FLOAT_COMPLEX_LE;
+
+    case 'D':
+        return _PY_FLOAT_BIG_ENDIAN ? \
+            IEEE_754_DOUBLE_COMPLEX_BE : IEEE_754_DOUBLE_COMPLEX_LE;
+
     /* Integers */
     case 'h':
         intsize = sizeof(short);
@@ -2227,6 +2351,52 @@ array__array_reconstructor_impl(PyObject *module, PyTypeObject *arraytype,
         }
         break;
     }
+    case IEEE_754_FLOAT_COMPLEX_LE:
+    case IEEE_754_FLOAT_COMPLEX_BE: {
+        Py_ssize_t i;
+        int le = (mformat_code == IEEE_754_FLOAT_COMPLEX_LE) ? 1 : 0;
+        Py_ssize_t itemcount = Py_SIZE(items) / 8;
+        const char *memstr = PyBytes_AS_STRING(items);
+
+        converted_items = PyList_New(itemcount);
+        if (converted_items == NULL) {
+            return NULL;
+        }
+        for (i = 0; i < itemcount; i++) {
+            PyObject *pycomplex = PyComplex_FromDoubles(
+                PyFloat_Unpack4(&memstr[i * 8], le),
+                PyFloat_Unpack4(&memstr[i * 8] + 4, le));
+            if (pycomplex == NULL) {
+                Py_DECREF(converted_items);
+                return NULL;
+            }
+            PyList_SET_ITEM(converted_items, i, pycomplex);
+        }
+        break;
+    }
+    case IEEE_754_DOUBLE_COMPLEX_LE:
+    case IEEE_754_DOUBLE_COMPLEX_BE: {
+        Py_ssize_t i;
+        int le = (mformat_code == IEEE_754_DOUBLE_COMPLEX_LE) ? 1 : 0;
+        Py_ssize_t itemcount = Py_SIZE(items) / 16;
+        const char *memstr = PyBytes_AS_STRING(items);
+
+        converted_items = PyList_New(itemcount);
+        if (converted_items == NULL) {
+            return NULL;
+        }
+        for (i = 0; i < itemcount; i++) {
+            PyObject *pycomplex = PyComplex_FromDoubles(
+                PyFloat_Unpack8(&memstr[i * 16], le),
+                PyFloat_Unpack8(&memstr[i * 16] + 8, le));
+            if (pycomplex == NULL) {
+                Py_DECREF(converted_items);
+                return NULL;
+            }
+            PyList_SET_ITEM(converted_items, i, pycomplex);
+        }
+        break;
+    }
     case UTF16_LE:
     case UTF16_BE: {
         int byteorder = (mformat_code == UTF16_LE) ? -1 : 1;
@@ -2932,7 +3102,7 @@ array_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
 PyDoc_STRVAR(module_doc,
 "This module defines an object type which can efficiently represent\n\
 an array of basic values: characters, integers, floating-point\n\
-numbers.  Arrays are sequence types and behave very much like lists,\n\
+numbers, complex numbers.  Arrays are sequence types and behave very much like lists,\n\
 except that the type of objects stored in them is constrained.\n");
 
 PyDoc_STRVAR(arraytype_doc,
@@ -2961,6 +3131,8 @@ The following type codes are defined:\n\
     'Q'         unsigned integer   8 (see note)\n\
     'f'         floating-point     4\n\
     'd'         floating-point     8\n\
+    'F'         float complex      8\n\
+    'D'         double complex     16\n\
 \n\
 NOTE: The 'u' typecode corresponds to Python's unicode character. On\n\
 narrow builds this is 2-bytes on wide builds this is 4-bytes.\n\
index 2648583c654a0402b899b8af1dbd434786b0e9e1..8a3fb4b515e4ac5ec4c247ae1f1b7bb8d333c89f 100644 (file)
@@ -335,8 +335,9 @@ PyDoc_STRVAR(array_array_byteswap__doc__,
 "\n"
 "Byteswap all items of the array.\n"
 "\n"
-"If the items in the array are not 1, 2, 4, or 8 bytes in size, RuntimeError is\n"
-"raised.");
+"If the items in the array are not 1, 2, 4, 8 or 16 bytes in size, RuntimeError\n"
+"is raised.  Note, that for complex types the order of\n"
+"components (the real part, followed by imaginary part) is preserved.");
 
 #define ARRAY_ARRAY_BYTESWAP_METHODDEF    \
     {"byteswap", (PyCFunction)array_array_byteswap, METH_NOARGS, array_array_byteswap__doc__},
@@ -778,4 +779,4 @@ array_arrayiterator___setstate__(PyObject *self, PyObject *state)
 
     return return_value;
 }
-/*[clinic end generated code: output=c993c3598085840e input=a9049054013a1b77]*/
+/*[clinic end generated code: output=9dcb2fc40710f83d input=a9049054013a1b77]*/