使用del是否不推荐?
我在代码中常常使用 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 个回答
我觉得没听过有人说 del
是个坏东西,至少没有比其他语言特性更糟糕。使用 del
和其他方法的选择,主要还是看你具体的使用场景。以下是一些适合用 del
的情况:
从当前作用域中删除变量。你可能会问,为什么要这样做?想象一下,你在声明一个模块,这个模块计算一个包变量,但使用这个模块的人根本不需要这个变量。虽然你可以为它创建一个全新的模块,但这样可能太复杂了,反而会让实际计算的内容变得不清晰。举个例子,你可能想要这样的:
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
来删除作用域中的变量。字典中的值也是如此,或者说任何通过名称或类似不可变引用(比如类属性、实例属性、字典值等)访问的东西。另一个情况是你想从列表或类似的有序序列中删除一个项目。从某种意义上说,这和第一种情况并没有太大区别(因为它们都可以作为键值容器访问,只是列表的键是有序的整数)。在这些情况下,你都想要删除对某个特定实例中存在的数据的引用(因为即使是类也是某种实例)。你是在进行原地修改。
那么,有序和特殊索引对列表来说意味着什么呢?列表的根本区别在于,进行原地修改会让你所有的旧索引基本上变得无用,除非你非常小心。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 中,通常不是处理列表的最佳方式。
使用 del
本身并不是坏事;不过,它有两个方面可能会让代码看起来不太好:
- 它是一个副作用,属于一系列步骤的一部分,单独看没有什么意义。
- 有可能
del
出现在那些手动管理内存的代码中,这通常说明对 Python 的作用域和自动内存管理理解不够。就像使用with
语句处理文件时比用file.close
更符合 Python 的习惯,使用作用域和上下文也比手动删除成员更符合习惯。
不过,这并不是绝对的——如果 del
这个关键词真的“坏”,它就不会出现在语言的核心部分。我只是想站在反方的角度,解释为什么有些程序员可能会称它为“坏”,并可能给你一个反驳的理由。;)
不,我觉得使用 del
并没有什么不好。实际上,在某些情况下,它几乎是唯一合理的选择,比如从字典中删除元素:
k = {'foo': 1, 'bar': 2}
del k['foo']
可能问题在于初学者对 Python 中变量的工作原理理解得不够透彻,所以使用(或误用) del
可能会让人感到陌生。
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会返回被删除的项目。这在需要把删除的项目转移到其他数组或数据结构时特别有用。除此之外,这两种方法在使用上没有太大区别。
其他回答主要从技术角度来看问题(比如,修改列表的最佳方法是什么),但我认为更重要的原因是,像切片这样的操作不会改变原始列表。
原因在于,通常这个列表是从某个地方来的。如果你修改了它,可能会无意中引发一些严重且难以发现的副作用,这可能会导致程序其他地方出现错误。即使你没有立即造成错误,也会让你的程序整体上更难理解、推理和调试。
比如,列表推导式和生成器表达式的好处在于,它们从来不会改变传入的“源”列表:
[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()
返回的是实际局部变量的一个副本。不过,这个回答在一般情况下仍然有效。