2.高级特性

高级特性

掌握了Python的数据类型,语句和函数,基本上就可以编出很多有用的程序了,比如构造一个1,3,5,...,99的列表,可以通过循环实现:

1
2
3
4
5
6
7
8
>>> L=[]
>>> n=1
>>> while n<=99:
... L.append(n)
... n=n+2
...
>>> print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]

取list的前一半元素,也可以通过循环实现。

但是Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。

基于这一思想,我们开始介绍Python中非常有用的高级特性,1行代码能实现的功能,绝对不用五行代码。


[TOC]


切片

取一个list或者tuple的部分元素是非常常见的操作,比如,一个list如下:

1
L=['Michael','Sarah','Tracy','Bob','Jack']

取前三个元素,应该怎么办?

笨方法:

1
2
3
Q=[L[0],L[1],L[2]]
>>> print(Q)
['Michael', 'Sarah', 'Tracy']

该方法如果扩展之后让去N个元素就没有办法了。取N个元素,也就是取序号为0-N-1的元素,可以用循环:

1
2
3
4
5
6
7
8
9
10
>>> r=[]
>>> n=3
>>> i=0
>>> while i<n:
... r.append(L[i])
... i=i+1
...
>>> print(r)
['Michael', 'Sarah', 'Tracy']
>>>

或者:

1
2
3
4
5
6
7
>>> r=[]
>>> n=3
>>> for i in range(n):
... r.append(L[i])
...
>>> r
['Michael', 'Sarah', 'Tracy']

对这种经常取指定索引范围的操作,用循环身份繁琐,因此,Python提供了切片(slice)操作符,能大大简化这种操作。

对应上面的问题,取前三个元素,用一行代码就可以搞定:

1
2
3
4
5
6
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
>>> L[1:3]
['Sarah', 'Tracy']
>>> L[:3]
['Michael', 'Sarah', 'Tracy']

L[0:3]表示,从索引0开始取,取到索引3为止,但不包括3.即索引取0,1,2,正好是三个元素。如果第一个索引是0,还可以省略,也可以从索引1开始,取出两个元素。

类似的,既然Python支持L[-1]取出倒数第一个元素,那么他同样支持倒数切片:

1
2
3
4
>>> L[-3:-1]
['Tracy', 'Bob']
>>> L[-5:]
['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']

切片操作十分有用。我们先创建一个0-99的数列:

1
>>> L=list(range(100))

可以通过切片取出某一段数列,比如前十个,后十个,11-20等:

1
2
3
4
5
6
>>> L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

前十个数,每两个去一个:

1
2
>>> L[:10:2]
[0, 2, 4, 6, 8]

所有数,每五个取一个:

1
2
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

如果什么都不写,只写[:]就可以复制一个list。

tuple也是list的一种,唯一区别是tuple不可变。因此tuple也可以用切片操作,操作的结果仍然是tuple:

1
2
3
4
5
6
7
>>> t=tuple(range(20))
>>> t
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
>>> t[:3]
(0, 1, 2)
>>> t[-12:-3]
(8, 9, 10, 11, 12, 13, 14, 15, 16)

字符串xxx也可以看成是一种list,每一个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍然是字符串:

1
2
3
4
5
>>> h='abcdefghi'
>>> h[:4]
'abcd'
>>> h[2:8:2]
'ceg'

在许多编程语言中,针对字符串提供了很多种截取函数,如substring,其实目的就是对字符串进行切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。

小结
有了切片操作,很多地方循环就不需要了。Python的切片非常灵活,一行代码就可以实现多行循环才能完成的操作

迭代

如果给定一个list或者tuple,我们可以通过for循环来遍历这个list或者tuple,这种遍历我们称为迭代(iteration)。

在Python中,迭代是通过for...in来完成的,而很多语言如Java,迭代是通过下标完成的,比如:

1
2
3
for(i=0;i<list.length;i++){
n=list[i]
}

可以看出,Python的for循环抽象程度要高于Java的for循环,因为Python的for循环不仅可以用于list或tuple上,还可以用于其他可迭代对象上,list这种数据类型虽然有下标,但很多其他数据类型是没有下标的。只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

1
2
3
4
5
6
7
>>> d={'a':1,'b':2,'c':3}
>>> for key in d:
... print(key)
...
c
a
b

其中因为dict的存储不是按照list的方式顺序排列,所以迭代出来的顺序很可能不一样。默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k,v in d.items()

由于字符串也是可迭代对象,因此,也可以用作for循环:

1
2
3
4
5
6
7
8
9
>>> for ch in 'abcdnf':
... print(ch)
...
a
b
c
d
n
f

所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象是list还是其他的数据类型。

如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc',Iterable) #str是否可迭代
True
>>> isinstance([1,2,3,4],Iterable) #str是否可迭代
True
>>> isinstance(123,Iterable) #str是否可迭代
False

如果对list实现类似Java那样的下标循环怎么办?Python中内置的enumerate函数可以把一个list编程索引-元素对,这样就可以在for循环中同事迭代索引和元素本身:

1
2
3
4
5
6
7
8
>>> for i, value in enumerate(['a','b','c','d','e']):
... print(i,value)
...
0 a
1 b
2 c
3 d
4 e

上面的for循环里,同时引用了两个变量,在Python里面是很常见的:

1
2
3
4
5
6
7
>>> for x,y in [(1,2),(3,4),(5,6),(7,8)]:
... print(x,y)
...
1 2
3 4
5 6
7 8

小结
任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型,只要符合迭代条件,就可以使用for循环。


列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式,如下:

1
2
>>> list(range(1,11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但是如果要生成[1*1,2*2,3*3,...,10*10],方法一是循环:

1
2
3
4
5
6
>>> L=[]
>>> for x in range(1,11):
... L.append(x*x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
>>> [x*x for x in range(1,11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x*x放在前面,后面跟上for循环,就可以把list创建出来。

for循环后面还可以加上if判断,这样我们局可以筛选出仅偶数的平方:

1
2
>>> [x*x for x in range(1,11) if x%2==0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

1
2
>>> [x+y for x in 'abcd' for y in 'ABCD']
['aA', 'aB', 'aC', 'aD', 'bA', 'bB', 'bC', 'bD', 'cA', 'cB', 'cC', 'cD', 'dA', 'dB', 'dC', 'dD']

三层和三层以上的循环就很少用了。

运用列表生成式可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

1
2
3
>> import os
>>> [d for d in os.listdir('.')]
['3D Objects', 'AppData', 'Application Data', 'Contacts', 'Cookies', 'Desktop', 'Documents', 'Downloads', 'Favorites', 'IntelGraphicsProfiles', 'Links', 'Local Settings', 'Music', 'My Documents', 'NetHood', 'NTUSER.DAT', 'ntuser.dat.LOG1', 'ntuser.dat.LOG2', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TM.blf', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TMContainer00000000000000000001.regtrans-ms', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TMContainer00000000000000000002.regtrans-ms', 'ntuser.ini', 'OneDrive', 'Pictures', 'PrintHood', 'Recent', 'Saved Games', 'Searches', 'SendTo', 'Templates', 'Videos', '「开始」菜单']

for循环其实可以同时使用两个甚至多个变量,如dictitems()可以同时迭代key和value:

1
2
3
4
5
6
>>> for k,v in d.items():
... print(k,'=',v)
...
c = 3
a = 1
b = 2

因此列表生成式也可以使用两个变量来生成list:

1
2
3
>>> d={'a':'b','c':'v','d':'g'}
>>> [k + '=' + v for k,v in d.items()]
['d=g', 'c=v', 'a=b']

最后把一个list中的所有字符串变为小写:

1
2
3
>>> L=['Hello','DSFA','DFAC']
>>> [s.lower() for s in L]
['hello', 'dsfa', 'dfac']

练习

L1=[‘Hello’,’World’,18,’Apple’,None],添加if语句,期待L2=[‘hello’, ‘world’, ‘apple’]

1
2
3
4
>>> L2=[s.lower() for s in L1 if isinstance(s,str)]
>>>
>>> L2
['hello', 'world', 'apple']

生成器

通过列表生成式,我们可以直接创建一个列表。但是受到内存的限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的空间,如果我们仅仅需要访问前面几个元素,后面的绝大多数元素占用的空间就被白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环过程中不断推算出后面的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多中方法。第一种很简答,只要把一个列表生成式的[] 改为(),就创建了一个generator:

1
2
3
4
5
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g=(x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000020E84788E60>

可以看出创建Lg的区别仅在于最外层是[]还是()L是一个list,而g是一个generator。我们可以直接打出list的么一个元素,但是我们怎么打出generator的每一个元素呢,如果要一个一个打,可以通过next()函数来获得generator的下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)

我们前面讲过,generator保存的是算法,每次调用next(g),就计算出下一个g的值,直到计算到最后一个元素,没有更多的元素时,抛出stopiteration的错误。generator非常强大,如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。比如著名的斐波拉契数列(Fibonacci),除了第一个和第二个数外,任意一个数可由前面两个数相加得到:
1,1,2,3,5,8,13,21,34…

斐波拉契数列用列表生成式写不出来,但是,用函数可以把它打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> def fib(max):
... n,a,b=0,0,1
... while n<max:
... print(b)
... a,b=b,a+b
... n=n+1
... return 'done'
...
>>> fib(10)
1
1
2
3
5
8
13
21
34
55
'done'

仔细看,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑非常类似generator。也就是说,上面个的函数和generator只有一步之遥。要把fib变为generator,只需要把print(b)改为yield b就可以了:

1
2
3
4
5
6
7
8
9
10
>>> def fib(max):
... n,a,b=0,0,1
... while n<max:
... yield b
... a,b=b,a+b
... n=n+1
... return 'done'
...
>>> fib(10)
<generator object fib at 0x0000020E84788BA0>

这就是定义generator的另一种方法。如果一个函数定义包含关键字yield,那么这个函数就不再是一个普通的函数,而是一个generator。

这里最难理解的是generator和函数的执行流程不一样。函数是顺序执行的,遇到return语句或者最后一行函数语句就返回。而变为generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句出继续执行。

举个例子,定义一个generator,一次返回数字1,3,5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> def odd():
... print('step1')
... yield 1
... print('step2')
... yield (3)
... print('step3')
... yield (5)
...
>>> o=odd()
>>> next(o)
step1
1
>>> next(o)
step2
3
>>> next(o)
step3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

可以看出,odd()不是普通函数,而是generator,在执行过程中,遇到yield就会中断,下次又继续执行。执行3次之后,没有yield可以执行了,所以第四次调用next(o)就报错。回到fib的例子,我们在循环过程中不断用yield,就会不断中断。当然要给循环设置一个条件来跳出循环,不然就会产生一个无限数列出来。同样的,把函数改为generator之后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for n in fib(10):
... print(n)
...
1
1
2
3
5
8
13
21
34
55

但是使用for循环时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> f=fib(6)
>>> while True:
... try:
... x=next(f)
... print('g',x)
... except StopIteration as e:
... print('Generator return value:',e.value)
... break
...
g 1
g 1
g 2
g 3
g 5
g 8
Generator return value: done

练习
杨辉三角,把每一行看做一个list,试着写一个generator,不断输出下一行的list:

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
>>> def tri():
... L=[1]
... while True:
... yield L
... L=[L[x]+L[x+1] for x in range(len(L)-1)]
... L.insert(0,1)
... L.append(1)
...
>>> n=0
>>> for t in tri():
... print(t)
... n=n+1
... if n==10:
... break
...
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]

小结
generator是非常强大的工具,在Python中,可以简单的把列表生成式改为generator。要理解generator的工作原理,他是在for循环的过程中不断计算出下一个元素,并在适当的条件结束循环。对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就要结束generator的指令,for循环随之结束。

请注意区分普通函数和generator函数,普通函数调用直接返回结果,generator函数调用实际返回一个generator对象。


迭代器

我们已经知道,可以直接作用关于for循环的数据类型有以下几种:一类是集合数据类型,如list,tuple,dict,set,str等;一类是generator,包括生成器和带yield的generator function。这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。可以使用isinstance()来判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
8
>>> isinstance([],Iterable)
True
>>> isinstance({},Iterable)
True
>>> isinstance('abc',Iterable)
True
>>> isinstance((x for x in range(10)),Iterable)
True

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator

可以用isinstance()来判断一个对象是否是Iterator对象:

1
2
3
4
>>> from collections import Iterator
>>> isinstance((x for x in range(10)),Iterator)
True
False

生成器都是Iterator对象,但是list,dict,str却不是。把这些Iterable变为Iterator可以使用iter()函数:

1
2
3
4
>>> isinstance(iter([]),Iterator)
True
>>> isinstance(iter('adsaf'),Iterator)
True

为什么list,dict,str不是Iterator???

这是因为Python的Iterator对象表示一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序的序列,但是我们却不能提前得知序列的长度,只能不断通过next()函数实现按需计算的下一个数据,所以Iterator的计算时惰性的,只有在需要的时候才会计算。Iterator甚至可以表示一个无限大的数据流,例如全体自然数,但是list是永远不可能存储全体的自然数的。

小结
凡是可以用作for循环的对象都是Iterable类型;
凡是可作用关于next()函数的对象都是Iterator类型,他们表示一个惰性计算的序列;
集合数据类型如listdictstr等都是Iterable,但是不是Iterator,不过可以通过iter()获得一个Iterator对象。

·Python上的for循环本质上是通过不断调用next()函数来实现的:

1
2
3
4
5
6
7
8
9
10
11
>>> for x in [1,2,3,4,5,6]:
... pass
... #等价的
>>> it=iter([1,2,3,4,5,6])
>>> while True:
... try:
... x=next(it)
... except StopIteration:
... break
...
>>>

Donate comment here