使用del是否不推荐?

52 投票
7 回答
9398 浏览
提问于 2025-04-18 04:32

我在代码中常常使用 del 来删除对象:

>>> array = [4, 6, 7, 'hello', 8]
>>> del(array[array.index('hello')])
>>> array
[4, 6, 7, 8]
>>> 

但是我听说很多人说使用 del 是不符合 Python 风格的。那么,使用 del 是不是一种不好的做法呢?

>>> array = [4, 6, 7, 'hello', 8]
>>> array[array.index('hello'):array.index('hello')+1] = ''
>>> array
[4, 6, 7, 8]
>>> 

如果不是,那为什么在 Python 中有这么多种方法可以实现同样的事情呢?其中有没有一种比其他方法更好呢?

选项 1:使用 del

>>> arr = [5, 7, 2, 3]
>>> del(arr[1])
>>> arr
[5, 2, 3]
>>> 

选项 2:使用 list.remove()

>>> arr = [5, 7, 2, 3]
>>> arr.remove(7)
>>> arr
[5, 2, 3]
>>> 

选项 3:使用 list.pop()

>>> arr = [5, 7, 2, 3]
>>> arr.pop(1)
7
>>> arr
[5, 2, 3]
>>> 

选项 4:使用切片

>>> arr = [5, 7, 2, 3]
>>> arr[1:2] = ''
>>> arr
[5, 2, 3]
>>> 

如果这个问题看起来像是个人意见,我感到抱歉,但我希望能得到一个合理的答案。如果两天内没有合适的回答,我会加赏金。

编辑:

因为有很多替代方法可以用来删除对象的某些部分,del 唯一的特点就是它可以完全移除对象:

>>> a = 'hello'
>>> b = a
>>> del(a)
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> b
'hello'
>>> 

但是,使用它来“取消定义”对象有什么意义呢?

另外,为什么下面的代码会改变两个变量:

>>> a = []
>>> b = a
>>> a.append(9)
>>> a
[9]
>>> b
[9]
>>> 

del 语句却达不到同样的效果呢?

>>> a = []
>>> b = a
>>> del(a)
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> b
[]
>>> 

7 个回答

2

我觉得没听过有人说 del 是个坏东西,至少没有比其他语言特性更糟糕。使用 del 和其他方法的选择,主要还是看你具体的使用场景。以下是一些适合用 del 的情况:

  1. 从当前作用域中删除变量。你可能会问,为什么要这样做?想象一下,你在声明一个模块,这个模块计算一个包变量,但使用这个模块的人根本不需要这个变量。虽然你可以为它创建一个全新的模块,但这样可能太复杂了,反而会让实际计算的内容变得不清晰。举个例子,你可能想要这样的:

    GLOBAL_1 = 'Some arbitrary thing'
    GLOBAL_2 = 'Something else'
    
    def myGlobal3CalculationFunction(str1, str2):
        # Do some transforms that consumers of this module don't need
        return val
    
    GLOBAL_3 = myGlobal3CalculationFunction(GLOBAL_1, GLOBAL_2)
    # Mystery function exits stage left
    del myGlobal3CalculationFunction
    

    基本上,大家都同意在必要时使用 del 来删除作用域中的变量。字典中的值也是如此,或者说任何通过名称或类似不可变引用(比如类属性、实例属性、字典值等)访问的东西。

  2. 另一个情况是你想从列表或类似的有序序列中删除一个项目。从某种意义上说,这和第一种情况并没有太大区别(因为它们都可以作为键值容器访问,只是列表的键是有序的整数)。在这些情况下,你都想要删除对某个特定实例中存在的数据的引用(因为即使是类也是某种实例)。你是在进行原地修改。

    那么,有序和特殊索引对列表来说意味着什么呢?列表的根本区别在于,进行原地修改会让你所有的旧索引基本上变得无用,除非你非常小心。Python 让你能够以非常语义化的方式表示数据:与其有一个 [actor, verb, object] 的列表并映射索引,不如有一个漂亮的字典 {'actor' : actor, 'verb' : verb, 'object' : object}。这种访问方式通常有很大的价值(这就是为什么我们通过名称而不是数字来访问函数):如果顺序不重要,为什么要让它变得僵化?如果顺序很重要,为什么要搞得所有对它的引用都无效(比如,元素的位置、元素之间的距离)?

问题在于,为什么你会直接通过索引删除列表中的值。在大多数情况下,修改列表中单个元素的操作可以通过其他函数轻松实现。想要删除某个特定值的项目?你可以用 remove。实现队列或栈?你可以用 pop(不要锁定它)。减少列表中某个实例的引用计数?用 l[i] = None 也能做到,而且你的旧索引仍然指向同样的东西。过滤元素?你可以用 filter 或者列表推导式。想要复制列表,去掉一些元素?你可以用 slice。想要去掉重复的、可哈希的元素?你可以用 list(set([])),或者如果你只需要遍历一次唯一元素,可以看看 itertools

在排除掉这些情况后,使用 del 删除列表的常见用例大约只有两个。首先,你可能是通过索引删除随机元素。这种情况其实不少,使用 del 完全合适。其次,你有存储的索引,表示你在列表中的位置(比如,在走廊里从一个房间走到另一个房间,有时随机销毁一个房间,这来自查理·辛编程风格指南)。如果你有多个索引指向同一个列表,这就变得棘手,因为使用 del 意味着所有索引都需要相应调整。这种情况不太常见,因为通常使用索引遍历的结构并不是从中删除元素的(例如,游戏棋盘的坐标网格)。不过确实会发生,比如在列表上使用 while 循环来轮询任务,并删除那些已经完成的。

这就指出了通过索引原地删除列表元素的根本问题:你基本上只能一次删除一个。如果你有两个要删除的元素的索引,然后先删除第一个?那么你的旧索引很可能不再指向原来的内容。列表是用来存储顺序的。由于 del 改变了绝对顺序,你就得在列表中走动或跳跃。再次强调,有一些合理的用例(例如,随机销毁),但还有很多其他情况其实是不合适的。特别是在新手 Python 程序员中,很多人会在函数上用 while 循环做一些糟糕的事情(也就是说,循环直到找到一个匹配输入的值,然后 del 这个索引)。del 需要一个索引作为输入,一旦执行,就会让所有指向该列表的现有索引指向完全不同的数据。如果维护多个索引,这就会变成一个维护噩梦。再次强调,这并不是说 del 就不好,只是说在 Python 中,通常不是处理列表的最佳方式。

6

使用 del 本身并不是坏事;不过,它有两个方面可能会让代码看起来不太好:

  1. 它是一个副作用,属于一系列步骤的一部分,单独看没有什么意义。
  2. 有可能 del 出现在那些手动管理内存的代码中,这通常说明对 Python 的作用域和自动内存管理理解不够。就像使用 with 语句处理文件时比用 file.close 更符合 Python 的习惯,使用作用域和上下文也比手动删除成员更符合习惯。

不过,这并不是绝对的——如果 del 这个关键词真的“坏”,它就不会出现在语言的核心部分。我只是想站在反方的角度,解释为什么有些程序员可能会称它为“坏”,并可能给你一个反驳的理由。;)

10

不,我觉得使用 del 并没有什么不好。实际上,在某些情况下,它几乎是唯一合理的选择,比如从字典中删除元素:

k = {'foo': 1, 'bar': 2}
del k['foo']

可能问题在于初学者对 Python 中变量的工作原理理解得不够透彻,所以使用(或误用) del 可能会让人感到陌生。

12

Python有很多种方法可以从列表中删除项目。这些方法在不同情况下都很有用。

# removes the first index of a list
del arr[0]

# Removes the first element containing integer 8 from a list
arr.remove(8)

# removes index 3 and returns the previous value at index 3
arr.pop(3)

# removes indexes 2 to 10
del arr[2:10]

所以它们各有各的用处。比如说,如果你想删除数字8,第二种方法比第一种或第三种更合适。因此,选择哪种方法主要是看具体情况和逻辑上的合理性。

编辑

arr.pop(3)del arr[3]的区别在于,pop会返回被删除的项目。这在需要把删除的项目转移到其他数组或数据结构时特别有用。除此之外,这两种方法在使用上没有太大区别。

49

其他回答主要从技术角度来看问题(比如,修改列表的最佳方法是什么),但我认为更重要的原因是,像切片这样的操作不会改变原始列表。

原因在于,通常这个列表是从某个地方来的。如果你修改了它,可能会无意中引发一些严重且难以发现的副作用,这可能会导致程序其他地方出现错误。即使你没有立即造成错误,也会让你的程序整体上更难理解、推理和调试。

比如,列表推导式和生成器表达式的好处在于,它们从来不会改变传入的“源”列表:

[x for x in lst if x != "foo"]  # creates a new list
(x for x in lst if x != "foo")  # creates a lazy filtered stream

当然,这种方法在内存上通常会更消耗资源,因为它会创建一个新列表,但使用这种方法的程序在数学上更纯粹,更容易理解。而且使用惰性列表(生成器和生成器表达式)时,内存开销会消失,计算只在需要时执行;可以查看http://www.dabeaz.com/generators/了解更多精彩内容。在设计程序时,不要过于关注优化(可以参考这个链接)。此外,从列表中删除一个项目是相当耗费资源的,除非它是链表(而Python的list并不是链表;有关链表的信息,请查看collections.deque)。


实际上,无副作用的函数和不可变 数据结构函数式编程的基础,这是一种非常强大的编程范式。

不过,在某些情况下,直接修改数据结构也是可以的(即使在函数式编程中,如果语言允许的话),比如当它是本地创建的,或者是从函数的输入复制过来的:

def sorted(lst):
    ret = list(lst)  # make a copy
    # mutate ret
    return ret

— 从外部来看,这个函数似乎是一个纯函数,因为它不会修改其输入(而且只依赖于它的参数,没有其他依赖(即没有(全局)状态),这是成为 函数的另一个要求)。

所以,只要你知道自己在做什么,del并不是坏事;但在使用任何形式的数据修改时要非常小心,并且只在必要时使用。总是从可能效率较低但更正确和数学上优雅的代码开始。

...并学习函数式编程 :)

附注:请注意,del也可以用来删除局部变量,从而消除对内存中对象的引用,这在与垃圾回收相关的目的上通常是有用的。


回答你的第二个问题:

关于你提到的del完全移除对象的第二部分问题——其实并不是这样的:在Python中,甚至无法告诉解释器/虚拟机从内存中移除一个对象,因为Python是一种垃圾回收语言(像Java、C#、Ruby、Haskell等),是运行时决定什么时机移除对象。

相反,当你在一个变量上调用del(与字典键或列表项不同)时,它的作用是:

del a

仅仅移除局部(或全局)变量,而移除变量指向的内容(在Python中,每个变量都持有一个指向其内容的指针/引用,而不是内容本身)。实际上,由于局部和全局变量在底层以字典的形式存储(请参见locals()globals()),del a等同于:

del locals()['a']

或者在全局作用域中应用时是del globals()['a']

所以如果你有:

a = []
b = a

你正在创建一个列表,并将其引用存储在a中,然后又创建了这个引用的另一个副本,存储在b中,而没有复制/触碰列表对象本身。因此,这两个调用影响的是同一个对象:

a.append(1)
b.append(2)
 # the list will be [1, 2]

而删除b与触碰b指向的内容没有任何关系:

a = []
b = a
del b
# a is still untouched and points to a list

此外,即使你在对象属性上调用del(例如del self.a),你实际上仍然是在修改一个字典self.__dict__,就像在执行del a时实际上是在修改locals()/globals()一样。

附注:正如Sven Marcnah所指出的,del locals()['a']在函数内部并不会真正删除局部变量a,这是正确的。这可能是因为locals()返回的是实际局部变量的一个副本。不过,这个回答在一般情况下仍然有效。

撰写回答