深入理解 Python 列表

译自:https://www.codementor.io/sheena/python-lists-in-depth-lrtmk7w4q#table-of-contents


Python 列表

Python 列表(list)和某些与列表类似的数据结构之间似乎有很多混乱。列表是什么?它与元组(tuple)和集合(set) 相比如何?字典(dict)呢?可变性是什么?什么是迭代器(iterator),它们值得关注吗?

本文旨在消除围绕这些问题的一些困惑。我们将从列表开始:如何创建它们以及如何与它们交互。在那之后,我们将看到一些利用循环、解析和递归来创建列表的例子。最后,我们将对列表和其他一些可迭代类型进行比较。


id 函数

在接下来的示例中,我们将在很大程度上利用 Python 内置的 id 函数,因此我们将从理解它开始。

>>> id
<built-in function id>

>>> print(id.__doc__)
Return the identity of an object.

This is guaranteed to be unique among simultaneously existing objects.
(CPython uses the object's memory address.)
>>>

>>> a = 1
>>> id(a)
10919424


>>> b = a
>>>
>>> id(b) == id(a)
True
>>>
>>> id(b) == id('spam')
False

简单来说,id 函数返回表示对象唯一标识的东西。如果我们有两个具有相同 id 输出的值,那么它们就是相同的对象。也就是说,它们在内存中的位置是一样的。

打个比方,假设有一个叫 Robert 的人。他的妈妈叫他 Robert,他的兄弟姐妹叫他 Rob,他的朋友叫他 Bob。Bob 的社会保障号码和 Robert 的社会保障号码是一样的。Rob 的社会保障号码和 Bob 的一样。

在 Python 中,就像这样:

>>> id(bob) == id(robert)
True
>>> id(rob) == id(bob)
True

这也意味着如果你做了一些改变 Rob 的事情,它会影响 Robert 和 Bob。如果 Rob 决定穿一件蓝色的T恤,那就意味着 Robert 和 Bob 穿的是一件蓝色的T恤——这是同一件T恤。所以:

>>> id(bob.shirt) == id(robert.shirt)
True
>>> id(rob.shirt) == id(bob.shirt)
True

列表

在本节中,我们将介绍一些示例,从简单的开始。如果你想继续的话,打开一个 Python3 shell。

创建列表

首先,是创建列表的一些基本语法。

让我们列出我们的第一个列表:

>>> l1 = [1,2,3]
>>> l1
[1, 2, 3]

>>> type(l1)
<class 'list'>

l1list 类的一个实例,列表是对象。l1 是一个整数列表。让我们创建一个字符串列表。

>>> l2 = ['a','b','c']
>>> l2
['a', 'b', 'c']

我们也可以从列表中引用其他变量。

>>> foo = 'b'
>>> l2 = ['a',foo,'c']
>>> l2
['a', 'b', 'c']

每个元素周围的逗号和空格对 Python 解释的方式没有影响,这意味着下列语句是相同的:

>>> l2 = ['a',foo,'c']
>>> l2 = ['a',foo,'c'  ,  ]

列表也可以扩展到多行,有时候这样做是为了可读性。

>>> l1 = [
...   1,      # 这也允许你
...   2,      # 对列表中特定元素
...   3       # 编写注释
... ]
>>> l1
[1, 2, 3]

一个列表可以包含多种类型的数据。例如,这个包含整数和字符串的列表:

>>> l3 = [1, 2, 3, 'a', 'b', 'c']
>>> l3
[1, 2, 3, 'a', 'b', 'c']

列表甚至可以包含列表!

>>> l4 = [1,2,[3,4,[5]],'a',3.2,True]
>>> l4
[1, 2, [3, 4, [5]], 'a', 3.2, True]

目前为止一切都很顺利。现在你可以识别并创建列表。

访问单个元素

现在我们将使用索引(indice)来访问列表中的单个元素:

>>> l4
[1, 2, [3, 4, [5]], 'a', 3.2, True]

索引从0开始。因此,第一个元素的索引是0,第二个元素的索引是1,以此类推。

>>> l4[0]
1
>>> l4[1]
2
>>> l4[2]
[3, 4, [5]]
>>> l4[3]
'a'
>>> l4[4]
3.2
>>> l4[5]
True

这是最后一个元素。如果我们试图访问列表末尾以外的元素,那么 Python 会引发一个 IndexError:

>>> l4[6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

如果你传入一个 Python 无法理解的索引,会得到一个 TypeError:

>>> l4['eggs']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not str
>>>
>>> l4[1.0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not float
>>>
>>> l4[1]
2

因此,根据这些 TypeError,列表索引必须是整数或者切片。我们已经讲过正整数,下面是一些负数的例子:

>>> l4
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[-1]
True
>>> l4[-2]
3.2
>>> l4[-3]
'a'
>>> l4[-4]
[3, 4, [5]]
>>> l4[-5]
2
>>> l4[-6]
1
>>> l4[-7]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

所以我们的列表有 6 个元素,最大整数索引是 5,最小整数索引是 -6。

切片(slice)和索引

列表索引必须是整数或者切片,在本节中,我们将介绍后者。切片是一种从现有列表创建新列表的机制,其中新列表只是原始列表的一个子集。要理解切片,你需要理解整数索引。

Slice 是 Python 默认定义的类,不需要导入任何模块。

>>> slice
<class 'slice'>
>>> print(slice.__doc__)
slice(stop)
slice(start, stop[, step])

Create a slice object.  This is used for extended slicing (e.g. a[0:10:2]).

好吧,这有点让人困惑。让我们看看如果我们使用切片作为索引会发生什么。

>>> l4
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l5 = l4[slice(0,6,1)]
>>> l5
[1, 2, [3, 4, [5]], 'a', 3.2, True]

l4l5 是一样的……还是不一样的?还记得我之前讲的 id 函数吗?这就是它发挥作用的地方:

>>> id(l4)==id(l5)
False

l5 看起来和 l4 一样,但实际上它只是一个拷贝。回到我们之前使用的类比:如果 l4Rob,那么 l5 就像 Rob 的双胞胎。我们叫她……Roberta。别急,还有更多:

>>> id(l4[0])==id(l5[0])
True
>>> id(l4[1])==id(l5[1])
True
>>> id(l4[2])==id(l5[2])
True

所以 l4l5 是不同的,但是它们的内容在内存中的位置是相同的。

继续我们的类比:假设 Robert 和 Roberta 有一书架他们共享的书。如果 Roberta 从书架上取下她的一本书,那么她就从书架上取下了 Robert 的一本书(因为是同一本书)。如果 Robert 把他的一本书丢进浴缸,那么他就把 Roberta 的一本书丢进浴缸(因为那是同一本书)(该死的罗伯特!)

就像 Roberta 和 Robert 共享同一本书一样,l4l5 共享相同的内容。一种技术上的说法是:l5l4 的浅拷贝。这可能有点出人意料,但是:

>>> id(l4[slice(0,6,1)]) == id(l4[slice(0,6,1)])
True

相同的初始列表与相同的 slice 返回相同的拷贝!

稍后我们将进一步探讨内存的意义。现在看起来很简单,但是我已经看到了一些非常奇怪的 bug。让我们仔细看看我们可以做出什么样的切片:

之前,我们这样做:

>>> l5 = l4[slice(0,6,1)]
>>> l5
[1, 2, [3, 4, [5]], 'a', 3.2, True]

Python 有一个很好的速记法,我们从现在开始使用它。这和我们之前做的是一样的。

>>> l5 = l4[0:6:1]
>>> l5
[1, 2, [3, 4, [5]], 'a', 3.2, True]

slice 的参数称为 start、stop 和 step。我们将改变每一个,看看它做些什么。让我们看看 start:

>>> l4[0:6:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[1:6:1]
[2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[2:6:1]
[[3, 4, [5]], 'a', 3.2, True]
>>> l4[-1:6:1]
[True]
>>> l4[-2:6:1]
[3.2, True]

一般来说:some_list[start:stop:step][0] == some_list[start]。但是如果你引用超出列表末尾的某个索引,slice 不会引发异常。

>>> l4[5000:6:1]
[]
>>> l4[-5000:6:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]

现在,让我们看看 stop。记住,l4 的最后一个元素是 True,并且索引为 5。

>>> l4[0:6:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:5:1]
[1, 2, [3, 4, [5]], 'a', 3.2]
>>> l4[0:4:1]
[1, 2, [3, 4, [5]], 'a']
>>> l4[0:-1:1]
[1, 2, [3, 4, [5]], 'a', 3.2]
>>> l4[0:-2:1]
[1, 2, [3, 4, [5]], 'a']
>>> l4[0:-3:1]
[1, 2, [3, 4, [5]]]

一般来说:some_list[start,stop:step][-1] = some_list[step 1]。同样,你可以引用超出列表末尾的索引:

>>> l4[0:5000:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:-5000:1]
[]

最后一个参数是 step:

>>> l4[0:6:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:6:2]
[1, [3, 4, [5]], 3.2]
>>> l4[0:6:3]
[1, 'a']
>>> l4[0:6:4]
[1, 3.2]
>>> l4[0:6:-1]
>>> []
>>> l4[6:0:-1]
[True, 3.2, 'a', [3, 4, [5]], 2]
>>> l4[6:0:-2]
[True, 'a', 2]

如果 step 是 1,那么我们返回列表中的每个元素。如果 step 是 2,我们返回每二个元素。负 step 值反转列表顺序,这意味着 startstop 需要适当的值。如果 step 是正的,那么 start < stop 是有意义的。但是如果 step 是负的,那么 stop < start 将更有意义。

做得好!现在你可以像专家一样使用切片了。还有一件事值得了解:并不是所有的切片参数都是必需的。

下面是一些你可以使用的快捷方式。

首先,默认的 step 是 1,所以如果你省略它,那么一切都没问题。以下都是等价的:

>>> l4[0:6:1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:6:]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:6]
[1, 2, [3, 4, [5]], 'a', 3.2, True]

只要有一个冒号 : 那么索引就被认为是切片。startstop 切片参数也是可选的。以下是等价的:

>>> l4[0:6]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[:6]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[0:]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[:]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[::]
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[::1]
[1, 2, [3, 4, [5]], 'a', 3.2, True]

如果你指定了一个负的 step,它也能工作。

>>> l4[5:-7:-1]
[True, 3.2, 'a', [3, 4, [5]], 2, 1]
>>> l4[:-7:-1]
[True, 3.2, 'a', [3, 4, [5]], 2, 1]
>>> l4[5::-1]
[True, 3.2, 'a', [3, 4, [5]], 2, 1]
>>> l4[::-1]
[True, 3.2, 'a', [3, 4, [5]], 2, 1]

很棒!这就是你所需要知道的关于切片的知识。

常见的列表操作和函数

列表不仅仅只有切片和索引。在这里,我们将介绍一些常见的函数。

append 将一个元素添加到列表的末尾。

>>> l=[]
>>> l
[]
>>> l.append('a')
>>> l
['a']
>>> l.append([1,2])
>>> l
['a', [1, 2]]
>>> l.append(1,2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: append() takes exactly one argument (2 given)

extend 用于连接列表。注意,这和 append 不同:

>>> l
['a', [1, 2]]
>>> l.extend()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: extend() takes exactly one argument (0 given)
>>> l.extend([])
>>> l
['a', [1, 2]]
>>> l.extend([1,2,3,4,5])
>>> l
['a', [1, 2], 1, 2, 3, 4, 5]

in 用于检查列表中是否存在某个元素:

>>> l
['a', [1, 2], 1, 2, 3, 4, 5]
>>> 'a' in l
True
>>> 'b' in l
False

not in 检查不存在的元素。

>>> 'b' not in l
True
>>> 'a' not in l
False

sort 在原地对一个列表进行排序,sorted 返回一个正确排序的新的列表:

>>> l = [111,4,22,6,30]
>>>
>>> sorted(l)
[4, 6, 22, 30, 111]
>>> l
[111, 4, 22, 6, 30]
>>> l.sort()
>>> l
[4, 6, 22, 30, 111]

最后,你可以使用赋值方法改变列表中的单个元素:

>>> l = [1,2,3]
>>> l
[1, 2, 3]
>>> l[0] = 'new'
>>> l
['new', 2, 3]
>>> l[3] = 'new'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range

可变性和内存问题

在本节中,我们将讨论一些问题。这种行为可能会导致一些非常令人困惑的bug。

如我们所见,列表是可变的。这意味着你可以在不创建全新列表的情况下更改列表的部分内容:

>>> l1 = [1,2,3]
>>> l1
[1, 2, 3]
>>> id(l1)
139786247365704
>>> original_id = id(l1)
>>> l1[1] = "updated"
>>> l1
[1, 'updated', 3]
>>> id(l1) == original_id
True

因此,如果我们有两个变量指向同一个列表,那么改变一个列表将同时改变两个变量:

>>> l1
[1, 'updated', 3]
>>> l2 = l1
>>> l2
[1, 'updated', 3]
>>> id(l1) == id(l2)
True
>>> l2.append('parrot')
>>> l2
[1, 'updated', 3, 'parrot']
>>> l1
[1, 'updated', 3, 'parrot']

这种行为甚至适用于嵌套的数据结构:

>>> # l4 contains a list. We're going to point at it from another variable
>>> l4
[1, 2, [3, 4, [5]], 'a', 3.2, True]
>>> l4[2]
[3, 4, [5]]
>>> l5 = l4[2]
>>> l5
[3, 4, [5]]
>>> l4[2][0]
3
>>> l5[0]
3
>>> l4[2][1]
4
>>> l5[1]
4
>>> l4[2][2]
[5]
>>> l5[2]
[5]
>>>
>>> id(l5) == id(l4[2])
True

所以 l4[2]l5 引用的是内存中的同一个区域(就像 Rob 和 Robert 是同一个人一样)。

>>> l5.extend(['cheddar','gouda'])
>>> l5
[3, 4, [5], 'cheddar', 'gouda']
>>> l4[2]
[3, 4, [5], 'cheddar', 'gouda']
>>> l4
[1, 2, [3, 4, [5], 'cheddar', 'gouda'], 'a', 3.2, True]

到目前为止一切顺利……下面是一些可能令人困惑的部分:

>>> id(l4[2]) == id(l5)
True
>>> l5
[3, 4, [5], 'cheddar', 'gouda']
>>> l5 = ['ni']
>>> l5
['ni']
>>> l4[2]
[3, 4, [5], 'cheddar', 'gouda']
>>> id(l4[2]) == id(l5)
False

这里发生的事情是:我们创建了一个新列表并将其赋值给 l5l5 现在指向一个全新的内存位置。

下面就在不创建新列表的情况下改变了列表:

>>> l5 = l4[2]
>>> id(l4[2]) == id(l5)
True
>>> l5
[3, 4, [5], 'cheddar', 'gouda']
>>> l5.clear()
>>> l5.append('ni')
>>> l5
['ni']
>>> l4[2]
['ni']
>>> id(l4[2]) == id(l5)
True

列表作为函数参数

我已经看到关于这个东西的混淆导致了很多 bug。

>>> def spam(some_list):
...     some_list.append(1)
...     return some_list
...
>>> l = []
>>> spam(l)
[1]
>>> spam(l)
[1, 1]
>>> spam(l)
[1, 1, 1]
>>> l
[1, 1, 1]

因此,每次调用 spam 函数时,传递到 spam 函数中的列表都会发生变化。很明显的,对吧?

这是默认列表参数的行为方式:

>>> def eggs(some_list=[]):
...     some_list.append(1)
...     return some_list
...
>>> eggs()
[1]
>>> eggs()
[1, 1]
>>> eggs()
[1, 1, 1]
>>> id(eggs())
139786247382088
>>> id(eggs())
139786247382088

eggs 不断返回相同的列表对象。该列表是在首次定义函数时创建的。现在,让我们创建一个新列表并将其传递进来:

>>>
>>> l=['something_new']
>>> eggs(l)
['something_new', 1]
>>> eggs(l)
['something_new', 1, 1]
>>> eggs(l)
['something_new', 1, 1, 1]
>>>
>>> # So now it behaves like our spam function
>>>
>>> # how about this:
>>>
>>> eggs([])
[1]
>>> eggs([])
[1]
>>> eggs([])
[1]

这个故事的寓意是:eggs 应该像女巫一样被烧掉。默认的可变参数是危险的!避开它们。下面是一种更安全的处理方式。现在,每次调用函数时默认行为都不会改变:

>>> def better_eggs(some_list=None):
...     if some_list == None:
...         some_list = []
...     some_list.append(1)
...     return some_list
...
>>> better_eggs()
[1]
>>> better_eggs()
[1]
>>> better_eggs()
[1]

列表对比 …

列表很好,但它并不是 Python 所能提供的唯一内置可迭代对象。在本节中,我们将对其他一些 Python 类型进行简要介绍:

字典

字典将键映射到值,值可以是任何值。键更具体一些,但相当灵活。

>>> d = {}
>>> type(d)
<class 'dict'>
>>>
>>> dir(d)
['__class__', '__contains__', ..., 'pop', 'popitem', 'setdefault', 'update', 'values']
>>>
>>> d = {1:2,'a':3, 'c': 'ddddd'}
>>>
>>> d
{1: 2, 'a': 3, 'c': 'ddddd'}

可以通过键访问各个值,而不是通过索引:

>>> d[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 0
>>> d[1]
2
>>> d['a']
3
>>> d['c']
'ddddd'

字典和列表一样是可变的。你可以对它们进行修改,而不需要创建一个全新的字典。这意味着字典可能成为我们之前讲过的相同陷阱的受害者。

>>> d['c'] = 'new value'
>>> d
{1: 2, 'a': 3, 'c': 'new value'}

>>> d['new key'] = 'new value'
>>> d
{1: 2, 'a': 3, 'new key': 'new value', 'c': 'new value'}

当试图从字典访问值时,可以提供默认值。

>>> x = d[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 0
>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> x = d.get(0)
>>> x
>>> print(x)
None
>>>
>>> x = d.get(0,"some default")
>>> x
'some default'

for 循环的工作方式与列表稍有不同。在列表中,for 循环遍历列表元素。字典中,循环迭代键(而不是值!)

>>> for key in d:
...     print(key)
...
1
a
new key
c

>>> for key in d:
...     print(key,' : ',d[key])
...
1  :  2
a  :  3
new key  :  new value
c  :  new value

集合

集合是包含唯一元素的无序集合:

>>> s = {1,2,3}
>>> s
{1, 2, 3}
>>> type(s)
<class 'set'>
>>> dir(s)
['__and__', '__class__',..., 'symmetric_difference_update', 'union', 'update']

由于它是无序的,因此不能通过索引访问元素。

>>> s[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object does not support indexing

要向集合添加内容,可以使用 add 函数。这意味着集合是可变的,就像列表和字典一样:

>>> s
{1, 2, 3}
>>> s.add(1)
>>> s
{1, 2, 3}
>>> s.add(55)
>>> s
{1, 2, 3, 55}
>>> s.add("parrot")
>>> s
{1, 2, 3, 'parrot', 55}

注意上面 “parrot” 的位置,集合不考虑顺序。

for 循环和 in 运算符对列表和集合的作用相同:

>>> for x in s:
...   print(x)
...
1
2
3
parrot
55
>>>

>>> s
{2, 3, 'parrot', 55}
>>> 2 in s
True
>>> 22 in s
False
>>> 2 not in s
False
>>> 22 not in s
True

元组

元组是元素的有序集合,但是是不可变的。如果你想对一个元组进行更改,您需要创建一个全新的元组。

>>> t = (1,2,3)
>>> type(t)
<class 'tuple'>
>>> dir(t)
['__add__', '__class__',..., 'count', 'index']

>>> t
(1, 2, 3)
>>> t[0]
1
>>> t[1]
2
>>> t[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range

索引赋值不起作用,因为它是不可变的:

>>> t[3] = 'boo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t[2] = 'boo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

for 循环和 in 操作符都能正常工作。

>>> for x in t:
...     print(x)
...
1
2
3

>>> t
(1, 2, 3)
>>> 1 in t
True
>>> 111 in t
False
>>> 1 not in t
False
>>> 111 not in t
True

猜你喜欢

转载自blog.csdn.net/qq_20084101/article/details/81704047