Skip to content

Commit b58343f

Browse files
gh-78959: Add order parameter to memoryview.cast()
memoryview.cast() now accepts a keyword-only order parameter. With order='F' it returns a zero-copy Fortran-contiguous (column-major) view of a flat buffer, mirroring the order argument of memoryview.tobytes(). Only 'C' (the default) and 'F' are accepted; the buffer is not copied.
1 parent 1fece44 commit b58343f

6 files changed

Lines changed: 136 additions & 19 deletions

File tree

Doc/library/stdtypes.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4867,14 +4867,20 @@ copying.
48674867
.. versionadded:: 3.2
48684868

48694869
.. method:: cast(format, /)
4870-
cast(format, shape, /)
4870+
cast(format, shape, /, *, order='C')
48714871
48724872
Cast a memoryview to a new format or shape. *shape* defaults to
48734873
``[byte_length//new_itemsize]``, which means that the result view
48744874
will be one-dimensional. The return value is a new memoryview, but
48754875
the buffer itself is not copied. Supported casts are 1D -> C-:term:`contiguous`
48764876
and C-contiguous -> 1D.
48774877

4878+
With a multidimensional *shape*, *order* selects the memory layout of
4879+
the result: ``'C'`` for C-contiguous (row-major, the default) or ``'F'``
4880+
for Fortran-contiguous (column-major). The buffer is still not copied,
4881+
so ``order='F'`` gives a zero-copy view over a buffer holding
4882+
column-major data.
4883+
48784884
The destination format is restricted to a single element native format in
48794885
:mod:`struct` syntax. One of the formats must be a byte format
48804886
('B', 'b' or 'c'). The byte length of the result must be the same
@@ -5022,8 +5028,20 @@ copying.
50225028
>>> y.nbytes
50235029
96
50245030

5031+
Interpret a flat buffer as a Fortran-contiguous (column-major) array::
5032+
5033+
>>> buf = bytes(range(6))
5034+
>>> y = memoryview(buf).cast('B', shape=[3, 2], order='F')
5035+
>>> y.f_contiguous
5036+
True
5037+
>>> y.tolist()
5038+
[[0, 3], [1, 4], [2, 5]]
5039+
50255040
.. versionadded:: 3.3
50265041

5042+
.. versionchanged:: next
5043+
Added the *order* parameter.
5044+
50275045
.. attribute:: readonly
50285046

50295047
A bool indicating whether the memory is read only.

Doc/whatsnew/3.16.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ New features
7575
Other language changes
7676
======================
7777

78+
* :meth:`memoryview.cast` now accepts an *order* parameter. With
79+
``order='F'`` it returns a zero-copy Fortran-contiguous (column-major)
80+
view of a flat buffer, which is useful for interfacing with column-major
81+
libraries.
82+
(Contributed by Serhiy Storchaka in :gh:`78959`.)
83+
7884

7985

8086
New modules

Lib/test/test_memoryview.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,54 @@ class ArrayMemorySliceSliceTest(unittest.TestCase,
689689

690690

691691
class OtherTest(unittest.TestCase):
692+
def test_cast_order(self):
693+
# gh-78959: cast(order=...) selects the result memory layout.
694+
b = bytearray(range(6))
695+
mv = memoryview(b)
696+
697+
c = mv.cast('B', shape=(3, 2)) # default: C-contiguous
698+
self.assertTrue(c.c_contiguous)
699+
self.assertFalse(c.f_contiguous)
700+
self.assertEqual(c.strides, (2, 1))
701+
self.assertEqual(c.tolist(), [[0, 1], [2, 3], [4, 5]])
702+
703+
f = mv.cast('B', shape=(3, 2), order='F') # Fortran-contiguous
704+
self.assertTrue(f.f_contiguous)
705+
self.assertFalse(f.c_contiguous)
706+
self.assertEqual(f.strides, (1, 3))
707+
self.assertEqual(f.tolist(), [[0, 3], [1, 4], [2, 5]])
708+
# zero-copy view: the flat physical bytes are unchanged
709+
self.assertEqual(f.tobytes('A'), bytes(range(6)))
710+
711+
# explicit 'C' is the default
712+
self.assertTrue(mv.cast('B', shape=(3, 2), order='C').c_contiguous)
713+
714+
# a wider format works too (strides scale by itemsize)
715+
m = memoryview(struct.pack('6i', *range(6))).cast('i', shape=(3, 2),
716+
order='F')
717+
self.assertTrue(m.f_contiguous)
718+
self.assertEqual(m.strides, (4, 12))
719+
self.assertEqual(m.tolist(), [[0, 3], [1, 4], [2, 5]])
720+
721+
# more than two dimensions
722+
m3 = memoryview(bytearray(range(24))).cast('B', shape=(2, 3, 4),
723+
order='F')
724+
self.assertTrue(m3.f_contiguous)
725+
self.assertEqual(m3.strides, (1, 2, 6))
726+
727+
# order is keyword-only and only the strings 'C'/'F' are accepted
728+
self.assertRaises(TypeError, mv.cast, 'B', (3, 2), 'F')
729+
self.assertRaises(TypeError, mv.cast, 'B', shape=(3, 2), order=None)
730+
self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='X')
731+
self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='A')
732+
733+
def test_cast_order_writable(self):
734+
# An F-contiguous cast shares memory with the original buffer.
735+
b = bytearray(6)
736+
f = memoryview(b).cast('B', shape=(3, 2), order='F')
737+
f[0, 1] = 9 # column-major: physical offset 3
738+
self.assertEqual(b[3], 9)
739+
692740
def test_ctypes_cast(self):
693741
# Issue 15944: Allow all source formats when casting to bytes.
694742
ctypes = import_helper.import_module("ctypes")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the *order* parameter to :meth:`memoryview.cast`.
2+
With ``order='F'`` it creates a zero-copy Fortran-contiguous (column-major)
3+
view of a flat buffer, mirroring the *order* argument of
4+
:meth:`memoryview.tobytes`.

Objects/clinic/memoryobject.c.h

Lines changed: 36 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/memoryobject.c

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,11 +1400,12 @@ copy_shape(Py_ssize_t *shape, const PyObject *seq, Py_ssize_t ndim,
14001400
return len;
14011401
}
14021402

1403-
/* Cast a 1-D array to a new shape. The result array will be C-contiguous.
1404-
If the result array does not have exactly the same byte length as the
1405-
input array, raise ValueError. */
1403+
/* Cast a 1-D array to a new shape. The result array will be C-contiguous
1404+
('C') or Fortran-contiguous ('F') according to 'order'. If the result
1405+
array does not have exactly the same byte length as the input array, raise
1406+
TypeError. */
14061407
static int
1407-
cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim)
1408+
cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim, char order)
14081409
{
14091410
Py_buffer *view = &mv->view;
14101411
Py_ssize_t len;
@@ -1425,7 +1426,10 @@ cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim)
14251426
len = copy_shape(view->shape, shape, ndim, view->itemsize);
14261427
if (len < 0)
14271428
return -1;
1428-
init_strides_from_shape(view);
1429+
if (order == 'F')
1430+
init_fortran_strides_from_shape(view);
1431+
else
1432+
init_strides_from_shape(view);
14291433
}
14301434

14311435
if (view->len != len) {
@@ -1469,21 +1473,32 @@ memoryview.cast
14691473
14701474
format: unicode
14711475
shape: object = NULL
1476+
*
1477+
order: int(accept={str}) = 'C'
14721478
14731479
Cast a memoryview to a new format or shape.
1480+
1481+
With a multidimensional *shape*, *order* selects the result
1482+
layout: 'C' for C-contiguous (row-major, the default) or 'F'
1483+
for Fortran-contiguous (column-major).
14741484
[clinic start generated code]*/
14751485

14761486
static PyObject *
14771487
memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
1478-
PyObject *shape)
1479-
/*[clinic end generated code: output=bae520b3a389cbab input=138936cc9041b1a3]*/
1488+
PyObject *shape, int order)
1489+
/*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/
14801490
{
14811491
PyMemoryViewObject *mv = NULL;
14821492
Py_ssize_t ndim = 1;
14831493

14841494
CHECK_RELEASED(self);
14851495
CHECK_RESTRICTED(self);
14861496

1497+
if (order != 'C' && order != 'F') {
1498+
PyErr_SetString(PyExc_ValueError, "order must be 'C' or 'F'");
1499+
return NULL;
1500+
}
1501+
14871502
if (!MV_C_CONTIGUOUS(self->flags)) {
14881503
PyErr_SetString(PyExc_TypeError,
14891504
"memoryview: casts are restricted to C-contiguous views");
@@ -1517,7 +1532,7 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
15171532

15181533
if (cast_to_1D(mv, format) < 0)
15191534
goto error;
1520-
if (shape && cast_to_ND(mv, shape, (int)ndim) < 0)
1535+
if (shape && cast_to_ND(mv, shape, (int)ndim, (char)order) < 0)
15211536
goto error;
15221537

15231538
return (PyObject *)mv;

0 commit comments

Comments
 (0)