Python3之高级特性
切片
基本概念
切片操作符是列表名称后跟一个方括号,
方括号中有一对可选的数字,并用冒号分割。
注意这与你使用的索引操作符十分相似。
记住数字是可选的,而冒号是必须的。
注意:也可以用相同的方法访问元组和字符串。
我们先创建一个0-99的数列:
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
可以通过切片轻松取出某一段数列。
前10个数: L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
前11-20个数: L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
后10个数: L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
切片操作符中的数字
第一个数(冒号之前)表示切片开始的位置,
第二个数(冒号之后)表示切片结束的位置,
第三个数(冒号之后)表示切片间隔数。
如果不指定第一个数,Python就从序列首开始。
如果没有指定第二个数,则Python会停止在序列尾。
注意,返回的序列从开始位置开始 ,刚好在结束位置之前结束。
即开始位置是包含在序列切片中的,而结束位置被排斥在切片外。
【左开有闭】
例如:
列表:shoplist=[0,1,2,3,4,5,6,7,8]
shoplist[1:3]返回:
从位置1开始,包括位置2,但是停止在位置3的一个序列切片,
因此返回一个含有两个项目的切片。
shoplist[:]返回:
整个序列的拷贝。
shoplist[::3]返回:
位置3,位置6,位置9…的序列切片。
切片操作符中的负数
负数用在从序列尾开始计算的位置。
shoplist[:-1]返回:
除了最后一个项目外包含所有项目的序列切片,
shoplist[::-1]返回:
倒序序列切片。
元组的切片操作
tuple也是一种list,唯一区别是tuple不可变。
因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
字符串的切片操作
字符串'xxx'也可以看成是一种list,每个元素就是一个字符。
因此,字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
在很多编程语言中:
针对字符串提供了很多各种截取函数(例如,substring),
其实目的就是对字符串切片。
Python没有针对字符串的截取函数,
只需要切片一个操作就可以完成,非常简单。
迭代(Iterable)
什么是迭代
如果给定一个list或tuple,
我们可以通过for循环来遍历这个list或tuple,
这种遍历我们称为迭代(Iteration)。
在Python中,迭代是通过for ... in来完成的,
而很多语言比如Java语言,迭代list是通过下标完成的,
比如Java代码:
for (i=0; i<list.length; i++) {
n = list[i];
}
可以看出,Python的for循环抽象程度要高于Java的for循环,
因为Python的for循环不仅可以用在list或tuple上,
还可以作用在其他可迭代对象上。
Iterable & Iterrator
可以直接作用于for循环的对象统称为可迭代对象(Iterable)。
可以被next()函数调用并不断返回下一个值的对象称为迭代器(Iterator)。
所有的Iterable均可以通过内置函数iter()来转变为Iterator。
对迭代器来讲,有一个__next()就够了。
在你使用for 和 in 语句时,
程序就会自动调用即将被处理的对象的迭代器对象,
然后使用它的next__()方法,直到监测到一个StopIteration异常。
字典的迭代
list这种数据类型虽然有下标,
但很多其他数据类型是没有下标的,
不过,只要是可迭代对象,无论有无下标,都可以迭代,
比如dict就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
因为dict的存储不是按照list的方式顺序排列,
所以,迭代出的结果顺序很可能不一样。
默认情况下,dict迭代的是key。
如果要迭代value,可以用for value in d.values():,
如果要同时迭代key和value,可以用for k, v in d.items()。
字符串的迭代
由于字符串也是可迭代对象,因此,也可以作用于for循环:
>>> for ch in 'ABC':
... print(ch)
...
A
B
C
所以,当我们使用for循环时,只要作用于一个可迭代对象,
for循环就可以正常运行,
而我们不太关心该对象究竟是list还是其他数据类型。
判断可迭代对象?
那么,如何判断一个对象是可迭代对象呢?
方法是通过collections模块的Iterable类型判断:
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
enumerate函数
如果要对list实现类似Java那样的下标循环怎么办?
Python内置的enumerate函数可以把一个list变成索引-元素对,
这样就可以在for循环中同时迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
for循环中的两个变量
上面的for循环里,同时引用了两个变量,
在Python里是很常见的,比如下面的代码:
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9
列表生成式
列表生成式即List Comprehensions:
是Python内置的非常简单却强大的可以用来创建list的生成式。
列表生成式案例1
要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
可以用list(range(1, 11)):
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?
方法一是循环:
>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
但是循环太繁琐,
而列表生成式则可以用一行语句代替循环生成上面的list:
>>>[x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
写列表生成式时,
把要生成的元素x * x放到前面,后面跟for循环,
就可以把list创建出来,简单、方便。
列表生成式案例2
for循环后面还可以加上if判断:
这样我们就可以筛选出仅偶数的平方:
>>>[x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
还可以使用两层循环:
可以生成全排列:
>>>[m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
列表生成式案例3
运用列表生成式,可以写出非常简洁的代码。
例如,列出当前目录下的所有文件和目录名,
可以通过一行代码实现:
# 注意:os.listdir可以列出文件和目录
>>> import os # 导入os模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')]
列表生成式案例4
for循环其实可以同时使用两个甚至多个变量,
比如dict的items()可以同时迭代key和value:
>>>d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
... print(k, '=', v)
...
y = B
x = A
z = C
因此,列表生成式也可以使用两个变量来生成list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
列表生成式案例5
把一个list中所有的字符串变成小写:
>>>L = ['Hello', 'World', 'IBM', 'Apple']
>>>[s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
迭代器(Iterator)
可以直接作用于for循环的数据类型有以下几种:
一类是集合数据类型,如list、tuple、dict、set、str等;
一类是generator,包括生成器和带yield的generator function。
可迭代对象:Iterable
这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
可以使用isinstance()判断一个对象是否是Iterable对象:
而生成器不但可以作用于for循环,
还可以被next()函数不断调用并返回下一个值,
直到最后抛出StopIteration错误表示无法继续返回下一个值了。
迭代器:Iterator
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象,
list、dict、str虽然是Iterable,却不是Iterator。
iter()函数
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter('abc'), Iterator)
True
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。
而使用list是永远不可能存储全体自然数的。
小结
凡是可作用于for循环的对象都是Iterable类型;
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
集合数据类型如list、dict、str等是Iterable,但不是Iterator,
不过可以通过iter()函数获得一个Iterator对象。
生成器(Generator)
生成器的概念
通过列表生成式,我们可以直接创建一个列表。
但是,受到内存限制,列表容量肯定是有限的。
而且,创建一个包含100万个元素的列表,会占用很大的存储空间,
如果我们仅仅需要访问前面几个元素,
那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,
那我们是否可以在循环的过程中不断推算出后续的元素呢?
这样就不必创建完整的list,从而节省大量的空间。
在Python中,这种一边循环一边计算的机制,称为生成器:generator。
生成器对象是一个迭代器:
但是它比迭代器对象多了一些方法,
它们包括send方法,throw方法和close方法。
这些方法,主要是用于外部与生成器对象的交互。
创建生成器之一
要创建一个generator,有很多种方法。
第一种方法很简单,
只要把一个列表生成式的[]改成(),就创建了一个generator:
>>> L = [x * x for x in range(10)] #列表生成式
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10)) #生成器
>>> g
<generator object <genexpr> at 0x1022ef630>
创建L和g的区别仅在于最外层的[]和(),
L是一个list,而g是一个generator。
:可以通过next()函数获得generator的下一个返回值,
指导抛出异常:“StopIteration”
>>> next(g)
0
>>> next(g) ,#以此类推
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
我们讲过,generator保存的是算法,
每次调用next(g),就计算出g的下一个元素的值,
直到计算到最后一个元素,
没有更多的元素时,抛出StopIteration的错误。
创建生成器之二
可以通过for循环创建生成器,因为generator也是可迭代对象:
>>>g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
所以,创建了一个generator后,
基本上是通过for循环来迭代它,
并且不需要关心StopIteration的错误。
创建生成器之三
generator非常强大。
如果推算的算法比较复杂,
用类似列表生成式的for循环无法实现的时候,
还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),
除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列用列表生成式写不出来,
但是,用函数把它打印出来却很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
注意,赋值语句:
a, b = b, a + b
相当于:
t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]
神器:不必显式写出临时变量就可以赋值。
上面的函数可以输出斐波那契数列的前N个数:
>>> fib(6)
仔细观察,可以看出,
fib函数实际上是定义了斐波拉契数列的推算规则,
可以从第一个元素开始,推算出后续任意的元素,
这种逻辑其实非常类似generator,但不是generator。
把fib函数变成generator,只需要把print(b)改为yield b就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
这就是定义generator的另一种方法。
如果一个函数定义中包含yield关键字,
那么这个函数就不再是一个普通函数,而是一个generator:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
生成器的执行流程
最难理解的就是generator和函数的执行流程不一样。
函数是顺序执行,遇到return语句或者最后一行函数语句就返回。
而变成generator的函数,在每次调用next()的时候执行,
遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
举个简单的例子,定义一个generator,依次返回数字1,3,5:
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)
调用该generator时,
首先要生成一个generator对象,
然后用next()函数不断获得下一个返回值:
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
可以看到,odd不是普通函数,而是generator,
在执行过程中,遇到yield就中断,下次又继续执行。
执行3次yield后,已经没有yield可以执行了,
所以,第4次调用next(o)就报错。
把函数改成generator后,基本上从来不会用next()来获取下一个返回值,
而是直接使用for循环来迭代:
>>> for n in fib(6):
... print(n)
...
容器
容器(container)
容器是一种把多个元素组织在一起的数据结构,
容器中的元素可以逐个地迭代获取,
可以用in, not in关键字判断元素是否包含在容器中。
在Python中,常见的容器对象有:
- list, deque, ….
- set, frozensets, ….
- dict, defaultdict, OrderedDict, Counter, ….
- tuple, namedtuple, …
- str
容器比较容易理解,
因为可以把它看作是一个盒子,里面可以塞任何东西。
从技术角度来说,当它可以用来询问某个元素是否包含在其中时,
那么这个对象就可以认为是一个容器,
比如 list,dict, set,tuples都是容器对象。
容器是一系列元素的集合,
str、list、set、dict、file、sockets对象都可以看作是容器,
容器都可以被迭代(用在for,while等语句中),
因此他们被称为可迭代对象。
可迭代对象(iterable)
但凡是可以返回一个迭代器的对象都可称之为可迭代对象,
迭代器(iterator)
那么什么迭代器呢?
它是一个带状态的对象,能在你调用next()方法的时候返回容器中的下一个值,
任何实现了__iter__和__next__()方法的对象都是迭代器,
__iter__返回迭代器自身,
__next__返回容器中的下一个值,
如果容器中没有更多元素了,则抛出StopIteration异常,
至于它们到底是如何实现的这并不重要。
迭代器就是实现了工厂模式的对象,
它在每次询问要下一个值的时候给你返
生成器(generator)
生成器简介
生成器算得上是Python语言中最吸引人的特性之一,
生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。
它不需要再像上面的类一样写__iter__()和__next__()方法了,
只需要一个yiled关键字。
生成器一定是迭代器(反之不成立),
因此任何生成器也是以一种懒加载的模式生成值。
它特殊的地方在于函数体中没有return关键字,
函数的返回值是一个生成器对象,
只有显示或隐示地调用next的时候才会真正执行里面的代码。
生成器在Python中是一个非常强大的编程结构,
可以用更少地中间变量写流式代码,
相比其它容器对象它更能节省内存和CPU,
当然它可以用更少的代码来实现相似的功能。
yield案例
生成器对象是一个迭代器:
但是它比迭代器对象多了一些方法,
它们包括send方法,throw方法和close方法。
这些方法,主要是用于外部与生成器对象的交互。
send方法有一个参数:
该参数指定的是上一次被挂起的yield语句的返回值。
案例如下:
def MyGenerator():
receive = yield 100
print('extra' + str(receive))
receive = yield 200
print('extra' + str(receive))
receive = yield 300
print('extra' + str(receive))
gen = MyGenerator()
print(next(gen)) #也可以用:print(gen.send(None))
print("first")
print(gen.send(2))
print("second")
print(gen.send(3))
print("third")
----
100
first
extra2
200
second
extra3
300
third
----
注意,yield语句是最具魔力的表达式:
这里“yield 100”整体被视为一个表达式,
send的内容会作为这个表达式的值,
随便左边用什么变量接收或者不接收,
总之yield就是send进来的那个值。
这个表达式变成send进来后的那个值之后,继续往下执行,
直到再次遇到yield,挂起。
如何启动生成器呢?
调用(gen.next()或gen.send(None))
上面代码的运行过程如下。
当调用gen.next()方法时:
python首先会执行MyGenerator方法的yield 1语句。
由于是一个yield语句,因此方法的执行过程被挂起,
而next方法返回值为yield关键字后面表达式的值,即为100。
当调用gen.send(2)方法时:
python首先恢复MyGenerator方法的运行环境。
同时,将表达式(yield 100)的返回值定义为send方法参数的值,即为2。
这样,接下来value=(yield 100)这一赋值语句会将value的值设为2。
继续运行会遇到yield value语句,MyGenerator方法再次被挂起。
同时,send方法的返回值为yield关键字后面表达式的值,
也即value的值,为200。
当调用gen.send(3)方法时:
首先恢复MyGenerator方法的运行环境。
同时,将表达式(yield value)的返回值定义为send方法参数的值,即为3。
这样,接下来value=(yield 200)这一赋值语句会将value的值置为3。
继续运行,MyGenerator方法执行完毕。
以此类推….
总的来说:
send方法和next方法唯一的区别,
在执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定,
从而实现与生成器方法的交互。
但是需要注意,在一个生成器对象没有执行next方法之前,
由于没有yield语句被挂起,所以执行send方法会报错。
当然,下面的代码是可以接受的:
gen = MyGenerator()
print( gen.send(None) )
因为当send方法的参数为None时,它与next方法完全等价。
但是注意,虽然上面的代码可以接受,但是不规范。
所以,在调用send方法之前,还是先调用一次next方法为好。