diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 886648e820f071d..408085fcadcbf95 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4867,7 +4867,7 @@ copying. .. versionadded:: 3.2 .. method:: cast(format, /) - cast(format, shape, /) + cast(format, shape, /, *, order='C') Cast a memoryview to a new format or shape. *shape* defaults to ``[byte_length//new_itemsize]``, which means that the result view @@ -4875,6 +4875,12 @@ copying. the buffer itself is not copied. Supported casts are 1D -> C-:term:`contiguous` and C-contiguous -> 1D. + With a multidimensional *shape*, *order* selects the memory layout of + the result: ``'C'`` for C-contiguous (row-major, the default) or ``'F'`` + for Fortran-contiguous (column-major). The buffer is still not copied, + so ``order='F'`` gives a zero-copy view over a buffer holding + column-major data. + The destination format is restricted to a single element native format in :mod:`struct` syntax. One of the formats must be a byte format ('B', 'b' or 'c'). The byte length of the result must be the same @@ -5022,8 +5028,20 @@ copying. >>> y.nbytes 96 + Interpret a flat buffer as a Fortran-contiguous (column-major) array:: + + >>> buf = bytes(range(6)) + >>> y = memoryview(buf).cast('B', shape=[3, 2], order='F') + >>> y.f_contiguous + True + >>> y.tolist() + [[0, 3], [1, 4], [2, 5]] + .. versionadded:: 3.3 + .. versionchanged:: next + Added the *order* parameter. + .. attribute:: readonly A bool indicating whether the memory is read only. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index da98f1ad6b9bc3b..6cce360cc9fb2ea 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -75,6 +75,12 @@ New features Other language changes ====================== +* :meth:`memoryview.cast` now accepts an *order* parameter. With + ``order='F'`` it returns a zero-copy Fortran-contiguous (column-major) + view of a flat buffer, which is useful for interfacing with column-major + libraries. + (Contributed by Serhiy Storchaka in :gh:`78959`.) + New modules diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index df5ab9f2c879ca3..3cb8a104faee5e6 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -689,6 +689,54 @@ class ArrayMemorySliceSliceTest(unittest.TestCase, class OtherTest(unittest.TestCase): + def test_cast_order(self): + # gh-78959: cast(order=...) selects the result memory layout. + b = bytearray(range(6)) + mv = memoryview(b) + + c = mv.cast('B', shape=(3, 2)) # default: C-contiguous + self.assertTrue(c.c_contiguous) + self.assertFalse(c.f_contiguous) + self.assertEqual(c.strides, (2, 1)) + self.assertEqual(c.tolist(), [[0, 1], [2, 3], [4, 5]]) + + f = mv.cast('B', shape=(3, 2), order='F') # Fortran-contiguous + self.assertTrue(f.f_contiguous) + self.assertFalse(f.c_contiguous) + self.assertEqual(f.strides, (1, 3)) + self.assertEqual(f.tolist(), [[0, 3], [1, 4], [2, 5]]) + # zero-copy view: the flat physical bytes are unchanged + self.assertEqual(f.tobytes('A'), bytes(range(6))) + + # explicit 'C' is the default + self.assertTrue(mv.cast('B', shape=(3, 2), order='C').c_contiguous) + + # a wider format works too (strides scale by itemsize) + m = memoryview(struct.pack('6i', *range(6))).cast('i', shape=(3, 2), + order='F') + self.assertTrue(m.f_contiguous) + self.assertEqual(m.strides, (4, 12)) + self.assertEqual(m.tolist(), [[0, 3], [1, 4], [2, 5]]) + + # more than two dimensions + m3 = memoryview(bytearray(range(24))).cast('B', shape=(2, 3, 4), + order='F') + self.assertTrue(m3.f_contiguous) + self.assertEqual(m3.strides, (1, 2, 6)) + + # order is keyword-only and only the strings 'C'/'F' are accepted + self.assertRaises(TypeError, mv.cast, 'B', (3, 2), 'F') + self.assertRaises(TypeError, mv.cast, 'B', shape=(3, 2), order=None) + self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='X') + self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='A') + + def test_cast_order_writable(self): + # An F-contiguous cast shares memory with the original buffer. + b = bytearray(6) + f = memoryview(b).cast('B', shape=(3, 2), order='F') + f[0, 1] = 9 # column-major: physical offset 3 + self.assertEqual(b[3], 9) + def test_ctypes_cast(self): # Issue 15944: Allow all source formats when casting to bytes. ctypes = import_helper.import_module("ctypes") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst new file mode 100644 index 000000000000000..c13ae1430b6f1ae --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst @@ -0,0 +1,4 @@ +Add the *order* parameter to :meth:`memoryview.cast`. +With ``order='F'`` it creates a zero-copy Fortran-contiguous (column-major) +view of a flat buffer, mirroring the *order* argument of +:meth:`memoryview.tobytes`. diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index a0cf3243edc08a0..d55e7c4ac475766 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -148,17 +148,21 @@ memoryview_release(PyObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(memoryview_cast__doc__, -"cast($self, /, format, shape=)\n" +"cast($self, /, format, shape=, *, order=\'C\')\n" "--\n" "\n" -"Cast a memoryview to a new format or shape."); +"Cast a memoryview to a new format or shape.\n" +"\n" +"With a multidimensional *shape*, *order* selects the result\n" +"layout: \'C\' for C-contiguous (row-major, the default) or \'F\'\n" +"for Fortran-contiguous (column-major)."); #define MEMORYVIEW_CAST_METHODDEF \ {"cast", _PyCFunction_CAST(memoryview_cast), METH_FASTCALL|METH_KEYWORDS, memoryview_cast__doc__}, static PyObject * memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, - PyObject *shape); + PyObject *shape, int order); static PyObject * memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -166,7 +170,7 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 2 + #define NUM_KEYWORDS 3 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -175,7 +179,7 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(format), &_Py_ID(shape), }, + .ob_item = { &_Py_ID(format), &_Py_ID(shape), &_Py_ID(order), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -184,17 +188,18 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"format", "shape", NULL}; + static const char * const _keywords[] = {"format", "shape", "order", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "cast", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[2]; + PyObject *argsbuf[3]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; PyObject *format; PyObject *shape = NULL; + int order = 'C'; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -209,9 +214,30 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec if (!noptargs) { goto skip_optional_pos; } - shape = args[1]; + if (args[1]) { + shape = args[1]; + if (!--noptargs) { + goto skip_optional_pos; + } + } skip_optional_pos: - return_value = memoryview_cast_impl((PyMemoryViewObject *)self, format, shape); + if (!noptargs) { + goto skip_optional_kwonly; + } + if (!PyUnicode_Check(args[2])) { + _PyArg_BadArgument("cast", "argument 'order'", "a unicode character", args[2]); + goto exit; + } + if (PyUnicode_GET_LENGTH(args[2]) != 1) { + PyErr_Format(PyExc_TypeError, + "cast(): argument 'order' must be a unicode character, " + "not a string of length %zd", + PyUnicode_GET_LENGTH(args[2])); + goto exit; + } + order = PyUnicode_READ_CHAR(args[2], 0); +skip_optional_kwonly: + return_value = memoryview_cast_impl((PyMemoryViewObject *)self, format, shape, order); exit: return return_value; @@ -506,4 +532,4 @@ memoryview_index(PyObject *self, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=3abf9c80cd49229a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=17a403895f4f778c input=a9049054013a1b77]*/ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index d92c7daff15dbf3..03d1b1d7b131486 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1400,11 +1400,12 @@ copy_shape(Py_ssize_t *shape, const PyObject *seq, Py_ssize_t ndim, return len; } -/* Cast a 1-D array to a new shape. The result array will be C-contiguous. - If the result array does not have exactly the same byte length as the - input array, raise ValueError. */ +/* Cast a 1-D array to a new shape. The result array will be C-contiguous + ('C') or Fortran-contiguous ('F') according to 'order'. If the result + array does not have exactly the same byte length as the input array, raise + TypeError. */ static int -cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim) +cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim, char order) { Py_buffer *view = &mv->view; Py_ssize_t len; @@ -1425,7 +1426,10 @@ cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim) len = copy_shape(view->shape, shape, ndim, view->itemsize); if (len < 0) return -1; - init_strides_from_shape(view); + if (order == 'F') + init_fortran_strides_from_shape(view); + else + init_strides_from_shape(view); } if (view->len != len) { @@ -1469,14 +1473,20 @@ memoryview.cast format: unicode shape: object = NULL + * + order: int(accept={str}) = 'C' Cast a memoryview to a new format or shape. + +With a multidimensional *shape*, *order* selects the result +layout: 'C' for C-contiguous (row-major, the default) or 'F' +for Fortran-contiguous (column-major). [clinic start generated code]*/ static PyObject * memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, - PyObject *shape) -/*[clinic end generated code: output=bae520b3a389cbab input=138936cc9041b1a3]*/ + PyObject *shape, int order) +/*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ { PyMemoryViewObject *mv = NULL; Py_ssize_t ndim = 1; @@ -1484,6 +1494,11 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, CHECK_RELEASED(self); CHECK_RESTRICTED(self); + if (order != 'C' && order != 'F') { + PyErr_SetString(PyExc_ValueError, "order must be 'C' or 'F'"); + return NULL; + } + if (!MV_C_CONTIGUOUS(self->flags)) { PyErr_SetString(PyExc_TypeError, "memoryview: casts are restricted to C-contiguous views"); @@ -1517,7 +1532,7 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, if (cast_to_1D(mv, format) < 0) goto error; - if (shape && cast_to_ND(mv, shape, (int)ndim) < 0) + if (shape && cast_to_ND(mv, shape, (int)ndim, (char)order) < 0) goto error; return (PyObject *)mv;