Skip to content

Commit 80fc48e

Browse files
Add list.discard() method
1 parent 862d5e5 commit 80fc48e

9 files changed

Lines changed: 166 additions & 21 deletions

File tree

‎Doc/builtins/stdtypes.rst‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1683,7 +1683,14 @@ application).
16831683

16841684
Lists implement all of the :ref:`common <typesseq-common>` and
16851685
:ref:`mutable <typesseq-mutable>` sequence operations. Lists also provide the
1686-
following additional method:
1686+
following additional methods:
1687+
1688+
.. method:: list.discard(value, /)
1689+
1690+
Remove the first item from the list where ``list[i] == value``
1691+
if such an item is present.
1692+
1693+
.. versionadded:: next
16871694

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

‎Doc/builtins/threadsafety.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ intermediate states since they modify multiple elements in place:
165165
lst.pop(idx) # idx not at the end of the list, shifts elements
166166
lst *= x # copies elements in place
167167
168-
The :meth:`~list.remove` method may allow concurrent modifications since
169-
element comparison may execute arbitrary Python code (via
170-
:meth:`~object.__eq__`).
168+
The :meth:`~list.remove` and :meth:`~list.discard` methods may allow
169+
concurrent modifications since element comparison may execute arbitrary
170+
Python code (via :meth:`~object.__eq__`).
171171

172172
:meth:`~list.extend` is safe to call from multiple threads. However, its
173173
guarantees depend on the iterable passed to it. If it is a :class:`list`, a

‎Doc/builtins/time-complexity.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ If you need to add or remove at both ends, consider using a
4747
- *O*\ (1)
4848
* - Delete item (``del l[k]``) [2]_
4949
- *O*\ (*n* - *k*)
50+
* - Discard (``l.discard(x)``, ``l.remove(x)``)
51+
- *O*\ (*n*)
5052
* - Iteration
5153
- *O*\ (*n*)
5254
* - Get slice (``l[i:j]``)

‎Doc/tutorial/datastructures.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ of the methods of list objects:
4343
:exc:`ValueError` if there is no such item.
4444

4545

46+
.. method:: list.discard(value, /)
47+
:noindex:
48+
49+
Remove the first item from the list whose value is equal to *value*, if there
50+
is such an item.
51+
52+
4653
.. method:: list.pop(index=-1, /)
4754
:noindex:
4855

‎Doc/whatsnew/3.16.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ Other language changes
107107
:func:`asyncio.print_call_graph` can see through them into the awaited code.
108108
(Contributed by Kumar Aditya in :gh:`157361`.)
109109

110+
* Added :meth:`list.discard`, which removes the first occurrence of a value
111+
from a list if it is present, like :meth:`set.discard`.
112+
(Contributed by Stan Ulbrych in :gh:`160724`.)
113+
110114

111115
Default interactive shell
112116
=========================

‎Lib/test/test_list.py‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
import textwrap
44
from test import list_tests, support
5-
from test.support import cpython_only
5+
from test.support import ALWAYS_EQ, NEVER_EQ, cpython_only
66
from test.support.script_helper import assert_python_failure, assert_python_ok
77
import pickle
88
import unittest
@@ -331,6 +331,66 @@ def test_preallocation(self):
331331
self.assertEqual(iter_size, sys.getsizeof(list([0] * 10)))
332332
self.assertEqual(iter_size, sys.getsizeof(list(range(10))))
333333

334+
def test_discard(self):
335+
a = [0, 0, 1]
336+
a.discard(1)
337+
self.assertEqual(a, [0, 0])
338+
a.discard(0)
339+
self.assertEqual(a, [0])
340+
a.discard(0)
341+
self.assertEqual(a, [])
342+
a.discard(0)
343+
self.assertEqual(a, [])
344+
345+
self.assertRaises(TypeError, a.discard)
346+
347+
a = [1, 2]
348+
a.discard(NEVER_EQ)
349+
self.assertEqual(a, [1, 2])
350+
a.discard(ALWAYS_EQ)
351+
self.assertEqual(a, [2])
352+
a = [ALWAYS_EQ]
353+
a.discard(1)
354+
self.assertEqual(a, [])
355+
a = [ALWAYS_EQ]
356+
a.discard(NEVER_EQ)
357+
self.assertEqual(a, [])
358+
a = [NEVER_EQ]
359+
a.discard(ALWAYS_EQ)
360+
self.assertEqual(a, [NEVER_EQ])
361+
362+
class BadExc(Exception):
363+
pass
364+
365+
class BadCmp:
366+
def __eq__(self, other):
367+
if other == 2:
368+
raise BadExc()
369+
return False
370+
371+
a = [0, 1, 2, 3]
372+
self.assertRaises(BadExc, a.discard, BadCmp())
373+
374+
class BadCmp2:
375+
def __eq__(self, other):
376+
raise BadExc()
377+
378+
d = list('abcdefghcij')
379+
d.discard('c')
380+
self.assertEqual(d, list('abdefghcij'))
381+
d.discard('c')
382+
self.assertEqual(d, list('abdefghij'))
383+
d.discard('c')
384+
self.assertEqual(d, list('abdefghij'))
385+
386+
# Handle comparison errors
387+
d = ['a', 'b', BadCmp2(), 'c']
388+
e = list(d)
389+
self.assertRaises(BadExc, d.discard, 'c')
390+
for x, y in zip(d, e):
391+
# verify that original order and values are retained.
392+
self.assertIs(x, y)
393+
334394
def test_count_index_remove_crashes(self):
335395
# bpo-38610: The count(), index(), and remove() methods were not
336396
# holding strong references to list elements while calling
@@ -356,6 +416,9 @@ def __eq__(self, other):
356416
with self.assertRaises(ValueError):
357417
lst.remove(lst)
358418

419+
lst = L([X()])
420+
lst.discard(lst)
421+
359422
# bpo-39453: list.__contains__ was not holding strong references
360423
# to list elements while calling PyObject_RichCompareBool().
361424
lst = [X(), X()]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :meth:`list.discard`, which removes the first occurrence of a value from
2+
a list if it is present.

‎Objects/clinic/listobject.c.h‎

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Objects/listobject.c‎

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3462,6 +3462,29 @@ list_count_impl(PyListObject *self, PyObject *value)
34623462
return PyLong_FromSsize_t(count);
34633463
}
34643464

3465+
static int
3466+
list_remove_lock_held(PyListObject *self, PyObject *value)
3467+
{
3468+
Py_ssize_t i;
3469+
3470+
for (i = 0; i < Py_SIZE(self); i++) {
3471+
PyObject *obj = self->ob_item[i];
3472+
Py_INCREF(obj);
3473+
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
3474+
Py_DECREF(obj);
3475+
if (cmp > 0) {
3476+
if (list_ass_slice_lock_held(self, i, i+1, NULL) < 0) {
3477+
return -1;
3478+
}
3479+
return 1;
3480+
}
3481+
else if (cmp < 0) {
3482+
return -1;
3483+
}
3484+
}
3485+
return 0;
3486+
}
3487+
34653488
/*[clinic input]
34663489
@critical_section
34673490
list.remove
@@ -3478,23 +3501,35 @@ static PyObject *
34783501
list_remove_impl(PyListObject *self, PyObject *value)
34793502
/*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/
34803503
{
3481-
Py_ssize_t i;
3504+
int res = list_remove_lock_held(self, value);
3505+
if (res < 0) {
3506+
return NULL;
3507+
}
3508+
if (res == 0) {
3509+
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
3510+
return NULL;
3511+
}
3512+
Py_RETURN_NONE;
3513+
}
34823514

3483-
for (i = 0; i < Py_SIZE(self); i++) {
3484-
PyObject *obj = self->ob_item[i];
3485-
Py_INCREF(obj);
3486-
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
3487-
Py_DECREF(obj);
3488-
if (cmp > 0) {
3489-
if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0)
3490-
Py_RETURN_NONE;
3491-
return NULL;
3492-
}
3493-
else if (cmp < 0)
3494-
return NULL;
3515+
/*[clinic input]
3516+
@critical_section
3517+
list.discard
3518+
3519+
value: object
3520+
/
3521+
3522+
Remove first occurrence of value if it is present.
3523+
[clinic start generated code]*/
3524+
3525+
static PyObject *
3526+
list_discard_impl(PyListObject *self, PyObject *value)
3527+
/*[clinic end generated code: output=4d8c1aee9a101f8c input=e1abe80fdc97f48e]*/
3528+
{
3529+
if (list_remove_lock_held(self, value) < 0) {
3530+
return NULL;
34953531
}
3496-
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
3497-
return NULL;
3532+
Py_RETURN_NONE;
34983533
}
34993534

35003535
static int
@@ -3685,6 +3720,7 @@ static PyMethodDef list_methods[] = {
36853720
LIST_EXTEND_METHODDEF
36863721
LIST_POP_METHODDEF
36873722
LIST_REMOVE_METHODDEF
3723+
LIST_DISCARD_METHODDEF
36883724
LIST_INDEX_METHODDEF
36893725
LIST_COUNT_METHODDEF
36903726
LIST_REVERSE_METHODDEF

0 commit comments

Comments
 (0)