为什么列表没有像字典一样安全的“get”方法?

400

为什么列表(list)没有像字典(dictionary)一样安全的“get”方法?

>>> d = {'a':'b'}
>>> d['a']
'b'
>>> d['c']
KeyError: 'c'
>>> d.get('c', 'fail')
'fail'

>>> l = [1]
>>> l[10]
IndexError: list index out of range

63
如果你用切片方式l[10:11]来获取列表中的元素,而不是使用索引方式l[10],即使要获取的元素不存在,也不会出现IndexError错误,而是会得到一个空的子列表。(如果元素存在,则该子列表将包含所需元素。) - jsbueno
95
与这里的某些人相反,我支持安全的.get的想法。它相当于l[i] if i < len(l) else default,但更易读、更简洁,并允许i是一个表达式,而不必重新计算它。 - Paul Draper
9
今天我希望有这样一个东西。我使用了一个昂贵的函数,它返回一个列表,但我只想要第一个元素,如果没有元素则返回“None”。如果能这样写就好了:“x = expensive().get(0, None)”,这样我就不必把昂贵函数的无用返回值放到一个临时变量中了。 - Ryan Hiebert
2
@Ryan 我的回答可能会对你有所帮助 https://dev59.com/_W435IYBdhLWcg3w7k2b#23003811 - Jake
8
Python问题跟踪器上有一个相关问题。信不信由你,但是mylist.get(myindex, mydefault)被拒绝了,转而采用(mylist[myindex:myindex+1] or mydefault)[0] :-/ - Rotareti
显示剩余6条评论
13个回答

162

最终,.get方法可能没有安全性,因为dict是一种关联集合(值与名称相关联),在不抛出异常的情况下检查键是否存在并返回其值是低效的,而访问列表元素时避免异常非常简单(因为len方法非常快)。 .get方法允许您查询与名称相关联的值,而不是直接访问字典中的第37个项目(这更像是要求列表的操作)。

当然,您可以轻松地自己实现:

def safe_list_get (l, idx, default):
  try:
    return l[idx]
  except IndexError:
    return default

您甚至可以将其猴子补丁到__builtins__.list构造函数中的__main__,但这将是一种不太普遍的更改,因为大多数代码不使用它。如果您只想在自己编写的列表中使用它,可以简单地继承list并添加get方法。


38
Python不允许猴子补丁内置类型,例如list - Imran
10
.get解决了列表所没有的问题——在获取可能不存在的数据时,避免异常的高效方式。确定有效的列表索引非常简单且高效,但是对于字典中的键值,没有特别好的方法来做到这一点。 - Nick Bastin
15
我认为这根本不是关于效率的问题——检查字典中是否存在某个键和/或返回一个项都是O(1)。从复杂度的角度来看,它们并不像检查len那样快,但在复杂度上它们都是O(1)。正确答案取决于典型的用法和语义... - Mark Longair
5
@Mark:并非所有的O(1)都是相同的。此外,dict仅在最理想情况下为O(1),并非所有情况都是如此。 - Nick Bastin
41
我认为人们在这里误解了重点。讨论不应该集中在效率上。请停止过早地进行优化。如果你的程序运行得太慢,那么要么是滥用了.get()方法,要么是代码(或环境)其他方面存在问题。使用这种方法的关键在于提高代码的可读性。使用“传统”技术需要在每个需要这样做的地方编写四行代码。而.get()技术只需要一行,并且可以轻松地与随后的方法调用链接起来(例如:my_list.get(2, '').uppercase())。 - Tyler Crompton
显示剩余7条评论

130

如果您想要获取第一个元素,可以使用此方法,例如my_list.get(0)

>>> my_list = [1,2,3]
>>> next(iter(my_list), 'fail')
1
>>> my_list = []
>>> next(iter(my_list), 'fail')
'fail'

我知道这不完全是你所要求的,但它可能有助于其他人。


11
比起函数式编程风格,不太符合 Pythonic。 - Eric
2
next(iter(my_list[index:index+1]), 'fail') 允许任何索引,而不仅仅是0。或者更少的FP但可以说更Pythonic,并且几乎肯定更可读:my_list[index] if index < len(my_list) else 'fail' - alphabetasoup
这种方法比Python 3.10.2中的一行if-else慢大约两倍。 - Yi Shen
1
@YiShen,你能在这里提供一下你的时间结果吗? - kaptan
如果你想要获取第n个元素,那么你可以使用next(iter(my_list[n:n+1]), 'fail') - Stef

71

可能是因为这对于列表语义并没有太多意义。但是,您可以通过子类化轻松创建自己的列表。

class safelist(list):
    def get(self, index, default=None):
        try:
            return self.__getitem__(index)
        except IndexError:
            return default

def _test():
    l = safelist(range(10))
    print l.get(20, "oops")

if __name__ == "__main__":
    _test()

7
这绝对是最符合Python编程风格的回答。请注意,您还可以提取子列表,这在Python中是安全的操作。如果有一个mylist = [1, 2, 3]的列表,您可以尝试使用mylist[8:9]来提取第9个元素,而不会触发异常。然后,您可以测试列表是否为空,并在它不为空时从返回的列表中提取单个元素。 - jose.angel.jimenez
1
这应该是被接受的答案,而不是其他非Pythonic的一行代码的hack,特别是因为它保持了与字典的对称性。 - Eric
4
仅仅因为需要一个好用的“get”方法,就去子类化自己的列表,并不具有Pythonic风格。可读性很重要,而每增加一个不必要的类,可读性都会下降。使用“try/except”方法,而不是创建子类,来达到相同的效果。 - Jeyekomon
3
@Jeyekomon 通过子类化减少样板代码是完全符合Python风格的。 - Keith
2
为什么不使用 return self[index] - timgeb
@Keith 完全正确。面向对象设计原则在这里得到了充分的体现。干得好! - Alex Vergara

49

不需要使用`get()`方法,对于列表来说这样使用是可以的。这只是用法上的区别。

>>> l = [1]
>>> l[10] if 10 < len(l) else 'fail'
'fail'

22
如果我们尝试使用-1来获取最新的元素,则此方法失败。 - ludo
请注意,这对于循环链表对象无效。此外,语法会导致我所谓的“扫描块”。当浏览代码以查看其功能时,这是一行会让我稍微停顿一下的代码。 - Tyler Crompton
内联if/else在旧版本的Python(如2.6或2.5)中不起作用。 - Eric
4
在Python中没有循环链表。如果你自己实现了一个,可以自由地添加 .get 方法(不过我不确定在这种情况下如何解释索引的含义,或者为什么它会失败)。 - Nick Bastin
2
一个处理负数下标越界的替代方案是 lst[i] if -len(lst) <= i < len(l) else 'fail' - mic

28

Credits to jose.angel.jimenez and Gus Bus.


对于“oneliner”粉丝...


如果你想要一个列表的第一个元素,或者在列表为空时想要一个默认值,请尝试:

liste = ['a', 'b', 'c']
value = (liste[0:1] or ('default',))[0]
print(value)

返回 a

并且

liste = []
value = (liste[0:1] or ('default',))[0]
print(value)

返回default


其他元素的示例...

liste = ['a', 'b', 'c']
print(liste[0:1])  # returns ['a']
print(liste[1:2])  # returns ['b']
print(liste[2:3])  # returns ['c']
print(liste[3:4])  # returns []

使用默认回退...

liste = ['a', 'b', 'c']
print((liste[0:1] or ('default',))[0])  # returns a
print((liste[1:2] or ('default',))[0])  # returns b
print((liste[2:3] or ('default',))[0])  # returns c
print((liste[3:4] or ('default',))[0])  # returns default

可能更短:

liste = ['a', 'b', 'c']
value, = liste[:1] or ('default',)
print(value)  # returns a

看起来你需要在等号前面、等号和后面的括号之间加上逗号。


更一般地说:

liste = ['a', 'b', 'c']
f = lambda l, x, d: l[x:x+1] and l[x] or d
print(f(liste, 0, 'default'))  # returns a
print(f(liste, 1, 'default'))  # returns b
print(f(liste, 2, 'default'))  # returns c
print(f(liste, 3, 'default'))  # returns default

使用 Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13) 进行测试


1
对于紧凑的语法,+1,但是一个安全的get()get_at_index()可能是一种更好的直观选择,至少对于那些还不熟悉Python切片符号的人来说。 - dreftymac

23
一个合理的做法是将列表转换为字典,然后使用get方法进行访问:

一个合理的做法是将列表转换为字典,然后使用get方法进行访问:

>>> my_list = ['a', 'b', 'c', 'd', 'e']
>>> my_dict = dict(enumerate(my_list))
>>> print my_dict
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
>>> my_dict.get(2)
'c'
>>> my_dict.get(10, 'N/A')

27
一种合理的解决办法,但并非“你能做的最好的事情”。 - tripleee
5
虽然非常低效。注意:可以使用dict(enumerate(my_list))代替zip range len的方法。 - Marian
4
这不是最好的选择,而是你能做的最糟糕的选择。 - erikbstack
10
如果你非常关心性能,那么在编程时不要选择像 Python 这样的解释型语言,因为这将是最糟糕的事情。但我认为使用字典作为解决方案既优雅又强大,符合 Python 的风格。不过,过早的优化总是很危险的,所以让我们先使用字典,稍后再看它是否成为瓶颈。 - Eric

22

试试这个:

>>> i = 3
>>> a = [1, 2, 3, 4]
>>> next(iter(a[i:]), 'fail')
4
>>> next(iter(a[i + 1:]), 'fail')
'fail'

1
我喜欢这个,尽管它需要先创建一个新的子列表。 - Rick
使用next(iter(a[i:i+1]), 'fail')next(iter(a[i:]), 'fail')更有效率。 - Stef

9

所以我对此进行了更多的研究,结果发现没有特定的方法来解决这个问题。当我发现list.index(value)时感到兴奋,它返回指定项的索引,但没有找到获取特定索引值的方法。如果您不想使用safe_list_get解决方案,以下是一些单行if语句,根据情况可以帮助您完成任务:

>>> x = [1, 2, 3]
>>> el = x[4] if len(x) > 4 else 'No'
>>> el
'No'

您也可以使用None代替'No',这更加合理。
>>> x = [1, 2, 3]
>>> i = 2
>>> el_i = x[i] if len(x) == i+1 else None

如果您只想获取列表中的第一项或最后一项,可以使用以下方法:

end_el = x[-1] if x else None

你也可以将它们转化为函数,但我仍然喜欢使用IndexError异常的解决方案。我用一个简化版的safe_list_get解决方案进行了实验,并使其更加简单(没有默认值):

def list_get(l, i):
    try:
        return l[i]
    except IndexError:
        return None

还没有进行基准测试来确定哪个更快。


1
不是很符合Python的风格。 - Eric
1
@Eric 哪个代码片段?我认为重新看一遍,用 try 和 except 是最有意义的。 - radtek
1
@Eric 我的理解是,这个问题不是特定于面向对象编程,而是关于“为什么列表没有类似于dict.get()的方法来返回默认值,而是必须捕获IndexError异常?”因此,这确实是关于语言/库特性(而不是OOP vs. FP上下文)。此外,你可能需要说明一下你对“pythonic”的使用,也许是WWGD(因为他对FP Python的鄙视是众所周知的),而不仅仅是满足PEP8/20。 - cowbert
你可能已经注意到,我被radtek卷入了FP中,因为这有助于他的论证。感觉像是在拖延时间。停止。 - Eric
1
“el = x[4] if len(x) == 4 else 'No'” - 你的意思是 len(x) > 4 吗?如果 len(x) == 4,那么 x[4] 就越界了。 - mic
显示剩余7条评论

5

字典用于查找。询问条目是否存在是有意义的。列表通常被迭代。不常询问L[10]是否存在,而是询问L的长度是否为11。


是的,我同意你的观点。但是我只解析了页面的相对URL“/group/Page_name”。将其按“/”分割并想要检查PageName是否等于某个页面。写起来可能更方便,例如[url.split('/').get_from_index(2, None) == "lalala"],而不是进行额外的长度检查、捕获异常或编写自己的函数。也许你是对的,这确实被认为是不寻常的。无论如何,我仍然不同意这个观点=) - Mikhail M.
@Nick Bastin:没有错。这全取决于编码的简单性和速度。 - Mikhail M.
如果您想在键为连续整数的情况下将列表用作更节省空间的字典,那么这也会非常有用。当然,负索引的存在已经阻止了这一点。 - Antimony

2

如果您:

  1. 想要一行代码实现,
  2. 不希望在不必要的情况下在您的代码路径中使用try / except,
  3. 希望默认值是可选的,

您可以使用以下代码:

list_get = lambda l, x, d=None: d if not l[x:x+1] else l[x]

使用方法如下:

>>> list_get(['foo'], 4) == None
True
>>> list_get(['hootenanny'], 4, 'ho down!')
'ho down!'
>>> list_get([''], 0)
''

1
list_get([""], 0) 失败应该返回 "",但实际上会返回 0 - Marek R
谢谢@MarekR! 我已经编辑了回答,修复了那个问题,并扩展了它以处理自定义默认值。 - Gus Bus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接