9. 数据类型 - 列表详解

茶桁的 AI 秘籍

Hi,大家好。我是茶桁。

最近几节课,我们都是在详细讲解 Python 内的数据类型,上一节课我们详细了解了字符串,这节课,让我们来详解一下列表。

首先,我们先有一个大的概念,列表,其实就是一组有序的数据组合;另外,列表中的数据是可以被修改的。也就是说,列表是一个可变序列类型

列表定义

如何在 Python 的定义列表,记住以下几点就可以了:

  • 可以使用中括号进行定义[]
  • 可以使用list()函数定义
  • 还可以使用列表推导式定义: [x for x in iterable]
  • 在定义列表中的元素时,需要在每个元素之间使用逗号(英文逗号),进行分隔。[1, 2, 3]
  • 列表中的元素可以是任意类型,通常用于存放同类项目的集合。

列表的基本操作

让我们先来定义一个列表:

1
2
3
4
5
6
7
items = [1, 2, 3, 4]
items2 = list('1234')

print(items,'\n', items2)
---
[1, 2, 3, 4]
['1', '2', '4', '5']

我们使用了最基本的两个方式来定义列表。至于列表推导式, 先不用着急,我们后面会单独讲它。

我们可以看到,刚才我刻意将itemitems两个列表定义了不同种类的元素,那他们到底能否拼接在一起?我们尝试一下列表的相加:

1
2
3
4
print(items + items2)

---
[1, 2, 3, 4, '1', '2', '3', '4']

没问题,两种不同类型的元素拼接到了一起,组成了一个新的列表。

让我们将这段代码搞的复杂一点,新的列表对于我要的模拟数据来说太少了,我想再增加 5 倍的长度:

1
2
3
4
print((items + items2) * 5)

---
[1, 2, 3, 4, '1', '2', '3', '4', 1, 2, 3, 4, '1', '2', '3', '4', 1, 2, 3, 4, '1', '2', '3', '4', 1, 2, 3, 4, '1', '2', '3', '4', 1, 2, 3, 4, '1', '2', '3', '4']

没毛病,也就是说,我将小学学到的基本数学运算用到这里完全适用。

那如果用到减法呢,虽然难以想象最后的结果,试试中可以:

1
2
3
4
print(items - items2)

---
TypeError: unsupported operand type(s) for -: 'list' and 'list'

果然是我想多了,完全不支持操作数类型。

那是不是关于列表的操作也就到此为止了?并不是,列表除了利用加和乘进行拼接和循环的操作之外,还有很多其他的基本操作,比如:

1
2
3
4
5
items[2] = 9
print(items, "\t",items[3])

---
[1, 2, 9, 4] 4

这里,我们利用了列表的下标操作修改了列表内的下标[2]的元素(第三个),并且将修改后的列表和列表内下标[3]的元素打印了出来。

有这样一种情况大家想过没有,这个列表呢,我并不知道有多长,但是我知道最后一个数字,现在我就想把最后一个数字取出来该怎么办?用len()获取长度之后再-1? 是不是太麻烦了?

还记得之前咱们讲过,下标是可以从后往前数的吗?

1
2
3
4
items[-1]

---
4

嗯,我想再这个列表添加几个数字:

1
2
3
4
items[4] = 10

---
IndexError: list assignment index out of range

哎,我似乎想的并不对。本以为原列表下标[3]是最后一个元素,那我多加一个下标就会再多加一个元素,可是似乎并不行。那么我们该怎么在列表内最佳元素呢?

可以尝试一下专门添加元素的append()函数:

1
2
3
4
5
6
items = [1, 2, 3, 4]
items.append(2)
print(items)

---
[1, 2, 3, 4, 2]

加是加了,可是我们之前是想加10的,现在不小心加成2了,不行,我要删了它。该怎么办?随便吧,我就记得 windows 的 CMD 命令中的删除文件似乎是del,试试看:

1
2
3
4
5
del items[-1]
print(items)

---
[1, 2, 3, 4]

居然成了... 这就神奇了。看起来,Python 并不是很难。不过我们这里不得不说,在 Python 中还有一个针对列表删除元素的方法:pop()

1
2
3
4
5
items = [1, 2, 3, 4]
items.pop()

---
[1, 2, 3]

pop([index=-1])函数专门用于移除列表中的一个元素,其中参数index为索引值,默认为-1,也就是说默认是从列表移除最后一个值。

1
2
3
4
5
6
7
# 将索引值改为从前数第一个
items = [1, 2, 3, 4]
items.pop(0)
print(items)

---
[2, 3, 4]

列表中的切片

在学习了列表的基本操作之后,我们来看看列表中的切片。提前说一声,在数据分析的应用中,对数据整理的过程绝大多数时候都需要用到列表的切片操作, 所以大家这部分要好好理解。

列表的切片操作基本语法其实很简单

list[开始值:结束值:步进值]

看起来很熟悉对吧?在我们之前介绍字符串相关的操作的时候,就是这种方式。其用法和字符串中也是如出一辙:

  • list[开始值:] 从开始值索引到列表的最后
  • list[:结束值]从列表最前面索引到结束值之前
  • list[开始值:结束值]按照给到的开始值开始索引,到结束值之前为止。

当然,除了这三个基本的操作之外还有list[::], list[::步进值], list[开始值::步进值], list[:结束值:步进值],list[开始值:结束值:步进值],我们下面一一的来看看,在字符串相关操作中没有特别理解的没关系,这里再来加深下印象:

1
2
# 先来定义一个列表方便后续操作
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']

从开始值索引到最后:

1
2
3
4
print(items[2:])

---
['Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP']

从下标[2]开始,也就是从第三个Ruby开始向后索引。

从最前面索引到结束值之前:

1
2
3
4
print(items[:2])

---
['Python', 'Java']

现在我们让这两个语言单独露脸,算是对它们进行补偿了。

从开始值索引到结束值之前:

1
2
3
4
print(items[2:3])

---
['Ruby']

哎,为什么只索引出来一个值?因为结束值为3,它之前不就是2吗。开始值也是2,那可不就只有一个值而已。

这回,我们把步进值加上:

1
2
3
4
5
# 加上步进值
print(items[0:-1:2])

---
['Python', 'Ruby', 'C++', 'JavaScript']

从最前面索引到最后,步进值为2,所以是隔一个索引一个。那为什么PHP没索引到?估计你又忘了,是索引到结束值之前,不包含结束值,自然PHP就没被索引到。

只有步进值会是什么情况?

1
2
3
4
5
# 只有步进值
print(items[::-2])

---
['PHP', 'JavaScript', 'C++', 'Ruby', 'Python']

步进值为负数,那显然是从后向前索引了。隔一个索引一个,等等,为啥第一个Python被索引到了? 那是因为,当我们开始值和结束值都没取值的情况下,默认是从头到尾索引,现在嘛,应该是从尾到头索引。也就是包含了头尾,不存在最后一个值之前,所以列表内的所有值都索引了一个遍,只是因为有步进值的关系,所以变成隔一个索引一个。

再让我们将所有值都去掉,只留下[::]试试看:

1
2
3
4
5
6
7
# 删掉所有值试试
print(items[::])
print(items[:])

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP']
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP']

从结果上看,中括号内一个冒号和两个冒号出来的结果是一样的。

在索引查找之后,我们来看看,利用切片的方式是否可以对列表内的元素进行更新和删除?

从指定下标开始,到指定下标前结束,并替换为对应的数据(容器类型数据,会拆分成每个元素进行赋值)

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
items[2:4] = [7, 8]
print(items)

---
[1, 2, 7, 8, 5]

指定的切片内的元素被替换掉了。

刚才我们使用切片替换元素的时候元素是一一对应的,那如果我们没有对应的话会发生什么?

1
2
3
4
5
6
7
# 切片范围大于添加元素的个数
items = [1, 2, 3, 4, 5]
items[2:6] = [7]
print(items)

---
[1, 2, 7]

结果并没有报错,而是将切片范围内的元素都移除之后添加了一个元素7。我们再试试其他的:

1
2
3
4
5
6
7
# # 切片范围小于添加元素的个数
items = [1, 2, 3, 4, 5]
items[2:3] = [7, 8, 9, 0]
print(items)

---
[1, 2, 7, 8, 9, 0, 4, 5]

可以看到,比起原本的列表,我们的值增加了。原本下标[2]的元素被移除之后,在这个位置插入了[7,8,9,0]四个元素。

以此,我们可以总结切片更新列表,实际上就是删除掉切片范围内的元素,再在原来的位置上插入新加的元素,并且将之后的元素向后移动。

那既然这样的话,我们是不是可以利用这种特性对列表内的元素进行删除?

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
items[2:4] = []
print(items)

---
[1, 2, 5]

没毛病,确实可以这么用。

当然,除了这种插入空列表的方式之外,还有其他方式可以删除列表内的指定元素, 还记得前面我们介绍的del方法吗?

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
del items[2:4]
print(items)

---
[1, 2, 5]

那既然我们可以用添加空列表的方式来删除列表内的元素,del是不是就没用武之地了?实际上,并非如此。del有一个特殊的用法,就是在利用步进值来跳着删除元素:

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
del items[0:6:2]
print(items)

---
[2, 4]

那聪明的小伙伴肯定想,添加空列表的方式也加上步进值就不行吗?我们来试试:

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
items[0:5:2] = []
print(items)

---
ValueError: attempt to assign sequence of size 0 to extended slice of size 3

报错提示我们,序列分配不正确。说明我们不能这样使用。如果要这样使用的话,替换的元素个数必须对应才行:

1
2
3
4
5
6
items = [1, 2, 3, 4, 5]
items[0:4:2] = [9, 10]
print(items)

---
[9, 2, 10, 4, 5]

列表相关函数(✨ 重点)

除了以上介绍的关于列表的一些方法之外,Python 还为我们提供了一些列表常用的相关函数:

len()

这个函数可以检测当前列表的长度,列表中元素的个数:

1
2
3
4
5
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
len(items)

---
9

count()

这个函数可以检测当前列表中指定元素出现的次数:

1
2
3
4
items.count('Python')

---
1

append()

这个函数前面我们已经介绍过了,就是向列表尾部追加新的元素,返回值为 None。

1
2
3
4
5
items.append('SQL')
print(items)

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP', 'SQL']

insert()

这个函数可以向列表中指定的索引位置添加新的元素。

1
2
3
4
5
items.insert(4, 'Go')
print(items)

---
['Python', 'Java', 'Ruby', 'Rust', 'Go', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP', 'SQL']

pop()

还记得我们之前删除列表中元素的时候介绍pop()函数吗?其实,pop()函数是对指定索引位置上的元素做出栈操作,然后返回出栈的元素

1
2
3
4
5
6
7
8
9
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
print(items.pop())
print(items.pop(2))
print(items)

---
PHP
Ruby
['Python', 'Java', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node']

默认的情况下,pop()是把列表的最后一个元素出栈,当给值之后,是将指定索引的元素进行出栈。

remove()

这个函数是专门删除特定元素用的,可以指定列表中的元素进行删除,只删除第一个,如果没有找到,则会报错。

1
2
3
4
5
6
7
8
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
items.remove('PHP')
print(items)
items.remove('Go')

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node']
ValueError: list.remove(x): x not in list

可以看到,第一个remove成功删除了PHP,但是第二个remove并未在列表中找到Go,所以报错。

index()

这个函数可以查找指定元素在列表中第一次出现的索引位置

1
2
3
4
5
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
items.index('PHP')

---
8

除此之外,index()还能接收索引值,当输入索引值的时候,index()会在指定范围内查找元素的索引位置:

1
2
3
4
5
6
7
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
print(items.index('Ruby', 0, 5))
items.index('PHP', 0, 5)

---
2
ValueError: 'PHP' is not in list

可以看到,指定范围内没有说要查找的元素的时候就会报错,告知元素不在列表内。

extend()

这个函数接收一个容器类型的数据,把容器的元素追加到原列表中

1
2
3
4
5
6
7
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
a = ['Go', 'MATLAB']
items.extend(a)
print(items)

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP', 'Go', 'MATLAB']

看到这,是不是感觉很像两个列表相加?那既然我们可以将两个列表相加了,这个方法似乎有些多余了。

这么想的小伙伴们,我们再来看两段示例:

1
2
3
4
5
6
7
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
a = (1, 2, 3)
items.extend(a)
print(items)

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP', 1, 2, 3]

另外一段:

1
2
3
4
5
6
7
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
a = (1, 2, 3)
items = items + a
print(items)

---
TypeError: can only concatenate list (not "tuple") to list

可以看到,第二段代码直接报错了。那说明,相加这个操作必须两个都是列表才可以,不支持列表和元组相加。可是extend()方法是支持将任意一个容器类型的数据中的元素追加到原列表中的。

我们再来多看一段:

1
2
3
4
5
6
7
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
a = '1234'
items.extend(a)
print(items)

---
['Python', 'Java', 'Ruby', 'Rust', 'C++', 'Swift', 'JavaScript', 'Node', 'PHP', '1', '2', '3', '4']

a定义为一段字符串,一样可以使用extend()来接收并追加到原列表内。

clear()

这个函数比较简单,就是清空列表内容

1
2
3
4
5
6
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
items.clear()
print(items)

---
[]

reverse()

这个函数可以对列表进行翻转

1
2
3
4
5
6
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
items.reverse()
print(items)

---
['PHP', 'Node', 'JavaScript', 'Swift', 'C++', 'Rust', 'Ruby', 'Java', 'Python']

sort()

该函数将对列表进行排序, 在默认的情况下,会对元素进行从小到大的排序

1
2
3
4
5
6
items = ['Python','Java','Ruby','Rust','C++','Swift','JavaScript','Node','PHP']
items.sort()
print(items)

---
['C++', 'Java', 'JavaScript', 'Node', 'PHP', 'Python', 'Ruby', 'Rust', 'Swift']

额,这样似乎并不明显,我们重新换个案例。不过大家也可以想想现在这段代码中,为什么会有这样的结果。

1
2
3
4
5
6
items = [9, 3, 5, 2, 1, 7, 8, 0, 6]
items.sort()
print(items)

---
[0, 1, 2, 3, 5, 6, 7, 8, 9]

嗯,这回明显了。

除了从小到大排序外,我们还可以将其从大到小排序,利用关键参数reverse来开启:

1
2
3
4
5
6
items = [9, 3, 5, 2, 1, 7, 8, 0, 6]
items.sort(reverse=True)
print(items)

---
[9, 8, 7, 6, 5, 3, 2, 1, 0]

OK,现在让我们来回过头来解释一下第一段代码中的结果:['C++', 'Java', 'JavaScript', 'Node', 'PHP', 'Python', 'Ruby', 'Rust', 'Swift'], 之所以会产生这样的结果,不是因为它按英文字母来排序,当然这么想也对但是不全对,而是因为它的排序依据是 ASCII 码,之前我们学习过,ASC II 码只包含了 128 个字符,仅仅是美国的标准,128 个字符里面都是西文码,那么如果中间包含了中文会怎样呢?

不如我们直接来看看:

1
2
3
4
5
6
items = [9, 3, 5, 2, 1, 7, 8, 0, '茶', '桁']
items.sort(reverse=True)
print(items)

---
TypeError: '<' not supported between instances of 'int' and 'str'

完了,直接报错。不过这个似乎和编码无关,而是数据类型的问题,告诉我们字符和整型之间不能排序。别问我怎么看懂的,我也是查字典。

知道是数据类型的问题就好办了,我们将数据类型变成一致的再试试:

1
2
3
4
5
6
items = ['9', '3', '5', '2', '1', '7', '8', '0', '茶', '桁']
items.sort(reverse=True)
print(items)

---
['茶', '桁', '9', '8', '7', '5', '3', '2', '1', '0']

居然成功了,那既然是 ASCII 码,那为什么还会支持中文排序呢?还记得我们除了介绍 ASCII 码之外,还介绍过一个 Unicode 编码。那即是说,Python 中的sort()排序的依据是Unicode编码。

当然,除了默认规则之外,我们还可以自己对排序进行干预,加上你想要的规则。sort(key)内的key参数可以接收一个函数,按照函数的处理结果进行排序:

1
2
3
4
5
6
items = [-5, -3, 5, 2, 0, -9, 12, 14, -1, -6]
items.sort(key=abs)
print(items)

---
[0, -1, 2, -3, -5, 5, -6, -9, 12, 14]

这一段是不是让小伙伴们想到之前我们在Python 的内置函数中介绍高阶函数的内容?没错,就是一样的。所以,我们这次就不对函数内部排序过程进行分析了,有兴趣的小伙伴可以回去看看第七节的内容。

深拷贝与浅拷贝

接着,让我们来看看关于拷贝的问题,先说浅拷贝。

说到浅拷贝,实际上是仅拷贝了列表中的一维元素,如果列表中存在二维元素或容器,则为引用而不是拷贝。使用copy函数或者copy模块中的copy函数拷贝的都是浅拷贝。

1
2
3
4
5
6
items = [1, 2, 3]
res = items.copy()
print(items, '\t', res)

---
[1, 2, 3] [1, 2, 3]

copy()之后的新列表和原列表内容上是一样的。

接着让我们来操作一下copy之后的res

1
2
3
4
5
6
7
8
9
items = [1, 2, 3]
res = items.copy()
del res[2]
print(items)
print(res)

---
[1, 2, 3]
[1, 2]

可以看到,对res进行操作完全不影响原列表的内容。这就说明,copy产生的新列表和原列表并不是一个列表,我们可以验证一下看看:

1
2
3
4
5
6
print(id(items))
print(id(res))

---
4636359872
4636086464

当我们用id()函数的时候,可以看到他们是两个完全不同的id

刚才我们定义的items是一个一维列表,接着让我们再来定义一个多维列表来尝试一下:

1
2
3
4
5
6
7
8
9
items = [1, 2, 3, 4, ['a', 'b', 'c']]
res = items.copy()
del res[3]
print(items)
print(res)

---
[1, 2, 3, 4, ['a', 'b', 'c']]
[1, 2, 3, ['a', 'b', 'c']]

我们可以看到,做删除操作之后,res内容变了,但是原列表items没变化。似乎和之前的并没有什么不同,让我们再继续试试:

1
2
3
4
5
6
7
del res[3][1]
print(res)
print(items)

---
[1, 2, 3, ['a', 'c']]
[1, 2, 3, 4, ['a', 'c']]

发生了什么?我们明明是操作的res而不是原列表items, 为什么items也发生了变化?难道是id是相同的吗?来,试试就知道了。

1
2
3
4
5
6
print(id(items))
print(id(res))

---
4636427264
4636085824

似乎并不相同。那既然不是同一个元素,为什么我们操作res的时候,items也会跟着一起变化?

别着急,让我们接着看下面的操作:

1
2
3
4
5
6
print(id(items[4])) # items 这个位置是列表['a', 'c']
print(id(res[3])) # res 这个位置是列表['a', 'c']

---
4635245952
4635245952

如何,一模一样对吧?这就说明,在items以及它的 copy 列表res中,这个嵌套的列表是同一份。这也就能解释为什么我们对res内的嵌套列表进行操作的时候, items也发生了变化。

这个就是我们在一开始说到的,copy仅仅是拷贝了列表中的一维元素,对二维元素和容器仅仅是引用,这个应用对象当然还是原来那个对象。所以,两者的id才是是同一个。

浅拷贝我们理解完之后,才看看什么是深拷贝。

深拷贝和浅拷贝比起来就有深度的多,嗯,这么讲是因为深拷贝不仅仅是拷贝了当前的列表,同时还把列表中的多维元素或容易也拷贝了一份,而不是像浅拷贝一样仅仅是引用。完成深拷贝的函数是copy模块中的deepcopy函数。

1
2
3
4
5
items = [1, 2, 3, ['a', 'b', 'c']]
res = items.deepcopy()

---
AttributeError: 'list' object has no attribute 'deepcopy'

额,尴尬。居然报错了... 似乎deepcopy并不是和copy函数一样的用法。

细心的小伙伴应该之前就注意到了,在介绍copy函数和deepcopy函数的时候,我都在强调是copy模块中的这句话,确实,我们在使用deepcopy的时候,是需要先引用模块再使用的,并且,使用方式也有一些不同:

1
2
3
4
5
6
7
import copy
items = [1, 2, 3, ['a', 'b', 'c']]
res = copy.deepcopy(items)
print(res)

---
[1, 2, 3, ['a', 'b', 'c']]

没错,我们这就对items完成了深拷贝,生成了新的列表res

那到底是否是真的深拷贝呢?让我们试一试:

1
2
3
4
5
6
7
8
9
10
11
print(id(items))
print(id(res))

print(id(items[3]))
print(id(res[3]))

---
4636282048
4634799872
4636285120
4637491072

没问题,我们打印出来的id各不一样,包括items内的二维列表以及res内的二维列表,id也都不同,说明确实是深拷贝。

不放心的小伙伴,我们再来更改列表元素测试一下:

1
2
3
4
5
6
7
8
del res[3][0]

print(res[3])
print(items[3])

---
['b', 'c']
['a', 'b', 'c']

可以看到,当我们更改res内的二维列表时,items并未发生改变。说明二维列表我们也一样完成了拷贝,而不是像浅拷贝一样仅是引用了。

列表推导式

在本文最开始,我们介绍列表的时候提过三种列表生成方式,包括直接定义列表, 用list函数,最后一个就是列表推导式。那我们接下来,就要详细讲讲列表推导式。

列表推导式提供了一个更简单的创建列表的方法,常见的用法是把某种操作应用于序列或可迭代的每个元素上,然后使用其结果来创建列表,或者通过满足某些特定条件元素来创建子序列。

采用一种表达式的当时,对数据进行过滤或处理,并且把结果组成一个新的列表。

哎,最怕就是定义和文字过多,让我们直接上示例吧。

基本的列表推导式使用方式

结果变量 = [变量或变量的处理结果 for 变量 in 容器类型数据]

现在,假设我们想要创建一个平方列表:

1
2
3
4
5
6
7
8
9
# 使用普通方法完成
items = []
for i in range(10):
items.append(i**2)

print(items)

---
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

这是用我们所学过的内容来进行创建,当然,我们还学过另外一种方式:

1
2
3
4
5
6
# 使用 map 函数和 list 完成
items = list(map(lambda x: x**2, range(10)))
print(items)

---
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

这里似乎有一点复杂,咱们还是来分析一下吧。

首先,我们创建了一个匿名函数lambda x:x**2, 再创建了一个可迭代对象range(10)

接着,我们给map函数传入了这两个参数,分别传给了func*iterables, 关于map函数,我们在第七节:内置函数中有讲解过,完了的小伙伴可以翻看前面复习一下。

map函数在对传入的可迭代数据中的每一个元素进行处理,然后返回一个新的迭代器, 最后用list函数将这个新的迭代器转换成了一个列表。

然后,我们将传入的func函数用一个匿名函数

没错,我们使用map函数和list也可以进行实现。

那么最后,让我们来看看列表推导式如何完成这个需求:

1
2
3
4
5
6
# 列表推导式
items = [i**2 for i in range(10)]
print(items)

---
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

简简单单一句话,比用map函数更简单的逻辑,就完成了我们的需求。这一句话的代码其实逻辑桁清晰,也很好理解:

首先,我们用for来进行循环传值给i, 接着,我们用i**2来得到我们期望的值,最后生成列表。本质上,其实和我们用的第一种普通方法是一样的。

接着我们再来看一个, 我们现在有一个字符串'1234', 想要得到这样一个列表[2, 4, 6, 8]。照例,从普通方法开始:

1
2
3
4
5
6
7
8
9
10
# 普通方法
str = '1234'
items = []
for i in str:
items.append(int(i)*2)

print(items)

---
[2, 4, 6, 8]

OK,没问题。我们继续:

1
2
3
4
5
6
7
8
9
items.clear()
print(items)

items = list(map(lambda x:int(x)*2, str))
print(items)

---
[]
[2, 4, 6, 8]

可以看到,我们先将items清空之后再继续操作的,这次我们用了list+map的方式,一样得到了我们想要的结果。

最后,当然是用列表推导式的方式:

1
2
3
4
5
6
7
8
9
items.clear()
print(items)

items = [int(i)*2 for i in str]
print(items)

---
[]
[2, 4, 6, 8]

同样,我们得到了想要的结果。

讲到这里了,我给大家秀一个小技巧,俗称骚操作,就是我们其实可以运用位运算操作符:

1
2
3
4
5
6
7
8
9
items.clear()
print(items)

items = [int(i) << 1 for i in str]
print(items)

---
[]
[2, 4, 6, 8]

具体代码执行的时候发生了什么,就算是给大家留个小思考题。提示:可以回头翻看下我们之前讲到的位运算符。

带有判断条件的列表推导式

除了基本的列表推导式,我们还有一种带有判断条件的列表推导式。

结果变量 = [变量或变量的处理结果 for 变量 in 容器类型数据 条件表达式]

相比起基本的列表推导式,我们现在多了一个条件表达式,那么我们该怎么利用呢?来个需求:从 0 ~ 9,求所有的偶数并且形成一个新的列表。这回,我们就只完成常规方法和列表推导式,对比着来观察一下:

1
2
3
4
5
6
7
8
9
10
# 常规方式
items = []
for i in range(10):
if i % 2 == 0:
items.append(i)

print(items)

---
[0, 2, 4, 6, 8]

很好,我们完成了需求。接下来,大家试试不看我下面写的代码,自己从常规方式思考下该怎么写,然后自己运行一下试试写对了没,最后,再和我写的对比一下看看咱们写的有没有区别。

1
2
3
4
5
items = [i for i in range(10) if i % 2 == 0]
print(items)

---
[0, 2, 4, 6, 8]

没错,就是这么简单,你做对了吗?

带有条件判断的多循环推导式

现在有这样一个需求,我们拿到两个列表[1,2,3], [3,1,4], 要将这两个列表中的元素两两组合,要求组合的元素不能重复:

1
2
3
4
5
6
7
8
9
10
# 常规方法
items = []
for x in [1, 2, 3]:
for y in [3, 1, 4]:
if x != y:
items.append((x,y))
print(items)

---
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

这样,我们就完成了刚才的需求。那用推导式该如何实现呢?

1
2
3
4
5
items = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y]
print(items)

---
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

没毛病,我们完全实现了刚才的需求。这个很好理解对吧?

让我们接着来看最后一个推导式的形式。

对于嵌套循环的列表推导式

这次我们直接写需求,然后上示例。

需求为,现在我们有一个 3x4 的矩阵,由 3 个长度为 4 的列表组成,我们现在要交换其行和列。

注意哦,这个行转列需求在处理数据的时候经常会用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 需求样例
'''
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]

==>

[
[1, 5, 9],
[2, 6, 10],
[3, 7, 11],
[4, 8, 12]
]
'''

来,让我们尝试着实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 首先,定义初始数据,大家可以直接 copy 我给到的矩阵数据

# 先定义数据
arr = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]

# 使用常规方法
items = []
for i in range(4):
res = []
for row in arr:
res.append(row[i])
items.append(res)
print(items)

# 使用列表推导式, 我们从内向外来写
items = [[row[i] for row in arr] for i in range(4)]
print(items)

---
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

这样,我们就完成了刚才的需求。我们拆解呢,还是从外层开始讲起:

首先,因为我们发现数据是 4 列,所以我们设定了一个range(4)来进行 4 次迭代,将0,1,2,3这四个下标分别传到内层循环。

然后我们开始在arr内循环找到当前的row, 循环会依次去寻找[1,2,3,4],[5,6,7,8],[9,10,11,12]。然后将每一个row中的寻找当前的row[i],并且填入一个新列表内。那么这三组列表中的row[i]就会是这样的:

row[1]分别为1, 5, 9, row[2]分别为2, 6, 10.... 依次类推。当外层循环完成之后,就正好是组成了新的四个新的列表,最后再将新列表依次传到items这个空列表内,就完成了。

那同样都是两次for循环嵌套,为什么上面那个案例就是顺序写的,内层for循环写在了后面,而下面这个案例的内层for循环就写到了前面呢?

好的,让我们来看看,如果将下面这个案例的内存for循环写在后面会是怎样的:

1
2
3
4
5
items = [row[i] for i in range(4) for row in arr]
items

---
[1, 5, 9, 2, 6, 10, 3, 7, 11, 4, 8, 12]

看到了吗?顺序还是对的,只是依次传入了数据,并未形成矩阵。那有小伙伴就说了,那是不是因为没在row[i]上加[]从而形成列表呢?

好的,让我们再来做一个实验:

1
2
3
4
5
items = [[row[i]] for i in range(4) for row in arr]
items

---
[[1], [5], [9], [2], [6], [10], [3], [7], [11], [4], [8], [12]]

可以看到,列表是形成了,但是却是一个元素占一个列表,并没形成我们想要的矩阵。

估计小伙伴们看出来了,在推导式中,因为变量或变量的处理结果必须放在前面,所以我们为了要形成矩阵内层新的row,所以才必须将处理结果和内层循环方法放在一起,并加上[]来确保这组结果能形成一个列表, 也就是我们现在这样:

1
2
3
4
5
items = [[row[i] for row in arr] for i in range(4)]
items

---
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

看一次没看懂的小伙伴,可以多看看,尝试着自己去分解,将其理解透彻。因为这个方法在我们后续的数据清洗中使用非常频繁。

小练习

  1. 为了能达到练习的目的,从这一节开始,所有练习可以不在课程中展示了。大家先做一下,然后可以在我下一节课中的源码中去找答案,然后来看看和自己做的是否一样。
  2. 以下所有练习必须使用列表推导式来实现
  3. 有些练习不止一个方法,大家尝试用多种方法来实现一下。
  4. 做完的小伙伴可以在课程后面留言讨论。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1. 让我们尝试将字典中的健值对转成`key = value`的数据格式
{'user':'admin', 'age':'20', 'phone':'133'} 转为 ['user=admin','age=20','phone=133']

# 2. 把列表中的所有字符全部转为小写
['A', 'CCCC', 'SHIss', 'Sipoa','Chaheng', 'Python','dsAhio']

# 3. x 是 0~5 之间的偶数,y 是 0~5 之间的奇数,把 x,y 组成一个元组,放到列表中

# 4. 使用列表推导式完成九九乘法表

# 5. 求 M, N 中矩阵和元素的乘积
'''
M = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

N = [
[2, 2, 2],
[3, 3, 3],
[4, 4, 4]
]
'''

最后,大家记得做练习并且留言,下课。

9. 数据类型 - 列表详解

https://hivan.me/Detailed-of-list/

作者

Hivan Du

发布于

2023-08-06

更新于

2024-01-16

许可协议

评论