Skip to content
Draft
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
10 changes: 9 additions & 1 deletion Doc/builtins/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,15 @@ application).

Lists implement all of the :ref:`common <typesseq-common>` and
:ref:`mutable <typesseq-mutable>` sequence operations. Lists also provide the
following additional method:
following additional methods:

.. method:: list.discard(value, /)

Remove the first item from the list where ``list[i] == value``
if such an item is present. Return ``True`` if an item was removed,
``False`` otherwise.

.. versionadded:: next

.. method:: list.sort(*, key=None, reverse=False)

Expand Down
6 changes: 3 additions & 3 deletions Doc/builtins/threadsafety.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ intermediate states since they modify multiple elements in place:
lst.pop(idx) # idx not at the end of the list, shifts elements
lst *= x # copies elements in place

The :meth:`~list.remove` method may allow concurrent modifications since
element comparison may execute arbitrary Python code (via
:meth:`~object.__eq__`).
The :meth:`~list.remove` and :meth:`~list.discard` methods may allow
concurrent modifications since element comparison may execute arbitrary
Python code (via :meth:`~object.__eq__`).

:meth:`~list.extend` is safe to call from multiple threads. However, its
guarantees depend on the iterable passed to it. If it is a :class:`list`, a
Expand Down
2 changes: 2 additions & 0 deletions Doc/builtins/time-complexity.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ If you need to add or remove at both ends, consider using a
- *O*\ (1)
* - Delete item (``del l[k]``) [2]_
- *O*\ (*n* - *k*)
* - Discard (``l.discard(x)``, ``l.remove(x)``)
- *O*\ (*n*)
* - Iteration
- *O*\ (*n*)
* - Get slice (``l[i:j]``)
Expand Down
7 changes: 7 additions & 0 deletions Doc/tutorial/datastructures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ of the methods of list objects:
:exc:`ValueError` if there is no such item.


.. method:: list.discard(value, /)
:noindex:

Remove the first item from the list whose value is equal to *value*, if there
is such an item. Return ``True`` if an item was removed, ``False`` otherwise.


.. method:: list.pop(index=-1, /)
:noindex:

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ Other language changes
:func:`asyncio.print_call_graph` can see through them into the awaited code.
(Contributed by Kumar Aditya in :gh:`157361`.)

* Added :meth:`list.discard`, which removes the first occurrence of a value
from a list if it is present and returns whether an item was removed.
(Contributed by Stan Ulbrych in :gh:`160724`.)


Default interactive shell
=========================
Expand Down
3 changes: 3 additions & 0 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,9 @@ def pop(self, i=-1):
def remove(self, item):
self.data.remove(item)

def discard(self, item):
return self.data.discard(item)

def clear(self):
self.data.clear()

Expand Down
60 changes: 60 additions & 0 deletions Lib/test/list_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,66 @@ def __eq__(self, other):
# verify that original order and values are retained.
self.assertIs(x, y)

def test_discard(self):
a = self.type2test([0, 0, 1])
self.assertIs(a.discard(1), True)
self.assertEqual(a, self.type2test([0, 0]))
self.assertIs(a.discard(0), True)
self.assertEqual(a, self.type2test([0]))
self.assertIs(a.discard(0), True)
self.assertEqual(a, self.type2test([]))
self.assertIs(a.discard(0), False)
self.assertEqual(a, self.type2test([]))

self.assertRaises(TypeError, a.discard)

a = self.type2test([1, 2])
a.discard(NEVER_EQ)
self.assertEqual(a, self.type2test([1, 2]))
a.discard(ALWAYS_EQ)
self.assertEqual(a, self.type2test([2]))
a = self.type2test([ALWAYS_EQ])
a.discard(1)
self.assertEqual(a, self.type2test([]))
a = self.type2test([ALWAYS_EQ])
a.discard(NEVER_EQ)
self.assertEqual(a, self.type2test([]))
a = self.type2test([NEVER_EQ])
a.discard(ALWAYS_EQ)
self.assertEqual(a, self.type2test([NEVER_EQ]))

class BadExc(Exception):
pass

class BadCmp:
def __eq__(self, other):
if other == 2:
raise BadExc()
return False

a = self.type2test([0, 1, 2, 3])
self.assertRaises(BadExc, a.discard, BadCmp())

class BadCmp2:
def __eq__(self, other):
raise BadExc()

d = self.type2test('abcdefghcij')
d.discard('c')
self.assertEqual(d, self.type2test('abdefghcij'))
d.discard('c')
self.assertEqual(d, self.type2test('abdefghij'))
d.discard('c')
self.assertEqual(d, self.type2test('abdefghij'))

# Handle comparison errors
d = self.type2test(['a', 'b', BadCmp2(), 'c'])
e = self.type2test(d)
self.assertRaises(BadExc, d.discard, 'c')
for x, y in zip(d, e):
# verify that original order and values are retained.
self.assertIs(x, y)

def test_index(self):
super().test_index()
a = self.type2test([-2, -1, 0, 0, 1, 2])
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_descrtut.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def merge(self, other):
'clear',
'copy',
'count',
'discard',
'extend',
'index',
'insert',
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ def __eq__(self, other):
with self.assertRaises(ValueError):
lst.remove(lst)

lst = L([X()])
lst.discard(lst)

# bpo-39453: list.__contains__ was not holding strong references
# to list elements while calling PyObject_RichCompareBool().
lst = [X(), X()]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`list.discard`, which removes the first occurrence of a value from
a list if it is present and returns whether an item was removed.
28 changes: 27 additions & 1 deletion Objects/clinic/listobject.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 54 additions & 15 deletions Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3462,6 +3462,29 @@ list_count_impl(PyListObject *self, PyObject *value)
return PyLong_FromSsize_t(count);
}

static int
list_remove_lock_held(PyListObject *self, PyObject *value)
{
Py_ssize_t i;

for (i = 0; i < Py_SIZE(self); i++) {
PyObject *obj = self->ob_item[i];
Py_INCREF(obj);
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
Py_DECREF(obj);
if (cmp > 0) {
if (list_ass_slice_lock_held(self, i, i+1, NULL) < 0) {
return -1;
}
return 1;
}
else if (cmp < 0) {

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.

This must be checked first as otherwise you may call the C API with an exception set. This error seems to br already present on main though (and not just here).

return -1;
}
}
return 0;
}

/*[clinic input]
@critical_section
list.remove
Expand All @@ -3478,23 +3501,38 @@ static PyObject *
list_remove_impl(PyListObject *self, PyObject *value)
/*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/
{
Py_ssize_t i;
int res = list_remove_lock_held(self, value);
if (res < 0) {
return NULL;
}
if (res == 0) {
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
return NULL;
}
Py_RETURN_NONE;
}

for (i = 0; i < Py_SIZE(self); i++) {
PyObject *obj = self->ob_item[i];
Py_INCREF(obj);
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
Py_DECREF(obj);
if (cmp > 0) {
if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0)
Py_RETURN_NONE;
return NULL;
}
else if (cmp < 0)
return NULL;
/*[clinic input]
@critical_section
list.discard
value: object
/
Remove first occurrence of value if it is present.
Return True if an item was removed, False otherwise.
[clinic start generated code]*/

static PyObject *
list_discard_impl(PyListObject *self, PyObject *value)
/*[clinic end generated code: output=4d8c1aee9a101f8c input=6330bc648285d994]*/
{
int res = list_remove_lock_held(self, value);
if (res < 0) {
return NULL;
}
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
return NULL;
return PyBool_FromLong(res);
}

static int
Expand Down Expand Up @@ -3685,6 +3723,7 @@ static PyMethodDef list_methods[] = {
LIST_EXTEND_METHODDEF
LIST_POP_METHODDEF
LIST_REMOVE_METHODDEF
LIST_DISCARD_METHODDEF
LIST_INDEX_METHODDEF
LIST_COUNT_METHODDEF
LIST_REVERSE_METHODDEF
Expand Down
Loading