Skip to content
Open
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
25 changes: 25 additions & 0 deletions Doc/c-api/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,28 @@ List Objects

Return a new tuple object containing the contents of *list*; equivalent to
``tuple(list)``.


.. c:function:: PyObject* PyList_AsTupleAndClear(PyObject *list)

.. index:: pair: built-in function; tuple

Return a new :class:`tuple` created from the existing :class:`list`
instance.

Transfers all items from *list* to the newly allocated :class:`tuple`.
Clears the input *list* on success.

Return a new :class:`tuple` on success.
Set an exception and return ``NULL`` on error.

This function is intended for building a :class:`tuple` instance via a
temporary *list* that is not exposed to user code.
To "freeze" an arbitrary list, for example one received from the user as an
argument, prefer :c:func:`PyList_AsTuple` to leave the source intact.

.. impl-detail::

Works with O(1) complexity.

.. versionadded:: next
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,10 @@ New features
API.
(Contributed by Nathan Goldbaum and Victor Stinner in :gh:`158842`.)

* Add :c:func:`PyList_AsTupleAndClear` function: similar to
:c:func:`PyList_AsTuple` but faster, and clear the input list.
(Contributed by Victor Stinner in :gh:`161635`.)

* Add the :ref:`PyUnicodeWriter API <pyunicodewriter>` to the limited C API.
(Contributed by Victor Stinner in :gh:`161172`.)

Expand Down
1 change: 1 addition & 0 deletions Include/cpython/listobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ PyList_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) {

PyAPI_FUNC(int) PyList_Extend(PyObject *self, PyObject *iterable);
PyAPI_FUNC(int) PyList_Clear(PyObject *self);
PyAPI_FUNC(PyObject*) PyList_AsTupleAndClear(PyObject *v);
1 change: 0 additions & 1 deletion Include/internal/pycore_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ typedef struct {
union _PyStackRef;

PyAPI_FUNC(PyObject *)_PyList_FromStackRefStealOnSuccess(const union _PyStackRef *src, Py_ssize_t n);
PyAPI_FUNC(PyObject *)_PyList_AsTupleAndClear(PyListObject *v);

#ifdef __cplusplus
}
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_capi/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,36 @@ def test_uninitialized_list_repr(self):
lst = _testlimitedcapi.list_new(3)
self.assertEqual(repr(lst), '[<NULL>, <NULL>, <NULL>]')

def test_as_tuple_and_clear(self):
list_as_tuple_and_clear = _testcapi.list_as_tuple_and_clear

lst = list("abc")
self.assertEqual(list_as_tuple_and_clear(lst), ('a', 'b', 'c'))
self.assertEqual(lst, [])
self.assertEqual(type(lst), list)

expected = tuple(object() for _ in range(10))
sub = ListSubclass(expected)
self.assertEqual(list_as_tuple_and_clear(sub), expected)
self.assertEqual(sub, [])
self.assertEqual(type(sub), ListSubclass)

lst = list(range(4096)) # test larger list
self.assertEqual(list_as_tuple_and_clear(lst), tuple(range(4096)))
self.assertEqual(lst, [])

# Get the empty tuple singleton if the list is empty
lst = []
self.assertIs(list_as_tuple_and_clear(lst), ())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: is () being a singleton a contract or implementation detail? if the former, it would be better to compare the equality.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah, that's a tricky question. It's more an implementation detail. But I would like to test this implementation detail, to check that we don't loose this optimization in a future refatoring.

self.assertEqual(lst, [])

# CRASHES list_as_tuple_and_clear(NULL)

# Wrong type
for obj in (tuple("abc"), {'key': 'value'}, 123):
with self.subTest(obj=obj):
self.assertRaises(TypeError, list_as_tuple_and_clear, obj)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyList_AsTupleAndClear` function: similar to
:c:func:`PyList_AsTuple` but faster, and clear the input list. Patch by
Victor Stinner.
10 changes: 10 additions & 0 deletions Modules/_testcapi/list.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,23 @@ test_list_api(PyObject *self, PyObject *Py_UNUSED(ignored))
}


// Test PyList_AsTupleAndClear()
static PyObject*
list_as_tuple_and_clear(PyObject *self, PyObject *arg)
{
NULLABLE(arg);
return PyList_AsTupleAndClear(arg);
}


static PyMethodDef test_methods[] = {
{"list_get_size", list_get_size, METH_O},
{"list_get_item", list_get_item, METH_VARARGS},
{"list_set_item", list_set_item, METH_VARARGS},
{"list_clear", list_clear, METH_O},
{"list_extend", list_extend, METH_VARARGS},
{"test_list_api", test_list_api, METH_NOARGS},
{"list_as_tuple_and_clear", list_as_tuple_and_clear, METH_O},
{NULL},
};

Expand Down
2 changes: 1 addition & 1 deletion Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -2066,7 +2066,7 @@ PySequence_Tuple(PyObject *v)
}
}
Py_DECREF(it);
PyObject *res = _PyList_AsTupleAndClear(list);
PyObject *res = PyList_AsTupleAndClear(_PyObject_CAST(list));
Py_DECREF(list);
return res;
fail:
Expand Down
8 changes: 7 additions & 1 deletion Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3346,8 +3346,14 @@ PyList_AsTuple(PyObject *v)
}

PyObject *
_PyList_AsTupleAndClear(PyListObject *self)
PyList_AsTupleAndClear(PyObject *v)
{
if (!PyList_Check(v)) {
PyErr_Format(PyExc_TypeError, "expected a list, got %T", v);
return NULL;
}
PyListObject* self = (PyListObject*)v;

assert(self != NULL);
PyObject *ret;
if (self->ob_item == NULL) {
Expand Down
3 changes: 1 addition & 2 deletions Python/intrinsics.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#include "pycore_genobject.h" // _PyAsyncGenValueWrapperNew
#include "pycore_interpframe.h" // _PyFrame_GetLocals()
#include "pycore_intrinsics.h" // INTRINSIC_PRINT
#include "pycore_list.h" // _PyList_AsTupleAndClear()
#include "pycore_object.h" // _PyObject_IsUniquelyReferenced()
#include "pycore_setobject.h" // _PySet_Freeze()
#include "pycore_pyerrors.h" // _PyErr_SetString()
Expand Down Expand Up @@ -198,7 +197,7 @@ list_to_tuple(PyThreadState* unused, PyObject *v)
into the tuple instead of copying them. */
assert(PyList_CheckExact(v));
assert(_PyObject_IsUniquelyReferenced(v));
return _PyList_AsTupleAndClear((PyListObject *)v);
return PyList_AsTupleAndClear(v);
}

static PyObject *
Expand Down
Loading