原文连接:廖雪峰老师的Python教程。笔者在学习了廖老师的教程之后做的笔记,一些细节之处有笔者自己见解。如有侵权,联系必删。
1. Python基础
1. 数据类型和变量
Python中可以直接处理的数据类型有以下几种:
整数、浮点数、字符串、布尔值、空值、变量、常量
-
整数
1000 和 1_000、1000000 和 1_000_000 是完全一样的。
整数运算永远是精确的,整数的除法也是。 -
浮点数
浮点数运算可能会有四舍五入的误差。 -
字符串
如果字符串中既包含 ’ 又包含 " 可以使用转义字符 \ 来标识:‘I’m “OK”!’ 表示的内容是 I’m “OK”!
-
转义字符的用途:
- \n 换行
- \t 制表符
- \本身也要转义
- \表示\
-
换行的一个demo:
>>>print('''abc ... bcd''') abc bcd
-
布尔值
空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。 -
变量
变量名必须是大小写英文、数字和_的组合,且不能用数字开头。
2. 字符编码
Unicode把所有语言都统一到一套编码里,解决乱码问题。
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
1. Python中的字符编码
对单个字符的编码,Python提供 ord() 函数获取字符的整数表示,chr() 函数把编码转换为对应的字符:
>>> ord('A')
65
>>> ord('a')
97
>>> ord('中')
20013
>>> chr(63)
'?'
>>> chr(69)
'E'
>>> chr(25932)
'敌'
知识点:
- 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
2. 格式化
1. Python用 % 运算符格式化数据:
demo:
注意:
- %s永远起作用,它会把任何数据类型转换为字符串
- 如果 % 是一个普通字符串,就需要对 % 转义, %% 输出 %
2. format()
3. list 和 tuple
list
- 添加元素:
- 删除元素:
tuple
不能pop和insert,可以根据切片获取,也可以直接删掉整个元组。
定义一个只包含一个元素的元组需要加个逗号:
tuple_demo = (1,)
元组的几个类型:
a = () b = (3,) c = (2, 3, 4)
4. dict 和 set
dict
和list比较,dict有以下几个特点:
- 查找和插入的速度极快,不会随着key的增加而变慢;
- 需要占用大量的内存,内存浪费多。
而list相反:
- 查找和插入的时间随着元素的增加而增加;
- 占用空间小,浪费内存很少。
所以,dict是用空间来换取时间的一种方法。
dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象。
这是因为 dict根据 key来计算 value的存储位置,如果每次计算相同的key得出的结果不同,那 dict内部就完全混乱了。 这个通过key计算位置的算法称为哈希算法(Hash)。
要保证 hash的正确性,作为 key的对象就不能变。
Python中的可变和不可变类型:
- 不可变类型:字符串、整数、元组;
- 可变类型:列表、集合
set
set = {1, 2, 3, 4} 不包含重复元素,set 与list 和tuple三者可以相互转换。
set 可以使用 remove删除指定元素。
5. if-elif-else 条件判断
形式:
if <条件判断1>:
<执行1>
elif <条件判断2>:
<执行2>
elif <条件判断3>:
<执行3>
else:
<执行4>
规则:
if 成立,eilf 和 else 都不执行;如果 if 下还是 if 则会每一个都判断。
6. 循环
for循环
列表生成式简单点的demo:
# 生成一个0~10的列表
>>> list(range(11))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
就不要使用下边这个了:
>>> [x for x in range(11)]
当然,如果有限制条件,下边这种写法还是不能忽视的。。
2. 函数
1. 函数的参数
1. 位置参数
def func(x):
return x + 2
# x 就是一个位置参数
2. 默认参数
def func(x=3):
return x
# x=3就是一个默认参数
注意:
- 必选参数在前,默认参数在后
- 设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数可以作为默认参数
- 默认参数必须指向不变类型
- 比如默认参数是列表时:
>>> def func(L=[]):
... L.append('a')
... return L
...
>>> func()
['a']
>>> func()
['a', 'a']
>>> func()
['a', 'a', 'a']
- 第一次调用看起来是正确的,但之后每次调用函数func()时都会追加一个’a’。因为默认参数 L=[ ] 列表时一个可变类型,它指向对象 [ ],每次调用该函数,如果改变了 L的内容,下次调用时,默认参数的内容就变了,不再是函数定义时的 [ ] 了。
设置默认参数的好处:
- 降低调用函数的难度。
调用默认参数:
- 按顺序提供默认参数;
- 不按顺序提供部分默认参数时,需要把参数名写上。
3. 可变参数(不定长参数)
定义:
- 参数前加 * 号。参数args接收到的是一个tuple元组,包含0到多个的任意个参数。
形式:
def func(*args):
pass
知识点:
-
如果需要把一个列表或元组或集合传入含可变参数的函数内,可以这样写:
def func(*args): pass num = [1, 2, 3] func(*num)
-
代码如下:
>>> def func(*nums):
... sum = 0
... for n in nums:
... sum = n * n
... return sum
...
>>> num_list = [1, 2, 3, 4, 5]
>>> func(*num_list)
25
>>> num_tuple = (1, 2, 3, 4, 5)
>>> func(*num_tuple)
25
>>> num_set = {1, 2, 3, 4, 5}
>>> func(*num_set)
25
- 当然也可以这样写,但是太繁琐:
>>> def func(*nums):
... sum = 0
... for n in nums:
... sum = n * n
... return sum
...
>>> num = [1, 2, 3]
>>> func(num[0], num[1], num[2])
9
4. 关键字参数
定义:
- 参数前加 ** 号。允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict。
形式:
def func(name, age, **kwargs):
pass
可以传入必传参数,也可以传入任意个参数:
>>> def person(name, age, **kwargs):
... print("name:",name,"age:",age,"other:",kwargs)
...
>>> person("王二狗", 25)
name: 王二狗 age: 25 other: {}
>>> person("王二狗", 25, city="北京")
name: 王二狗 age: 25 other: {'city': '北京'}
关键字参数作用:
关键字参数可以扩展函数的功能。
比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
**自动拆包字典的数据:person(info)
>>> def person(name,age,**kw):
... print('name:',name,'age:',age,'other:',kw)
...
>>> info={'name':'jason','gender':'male','age':25,'city':'beijing','job':'coder'}
>>> person(**info)
name: jason age: 25 other: {'gender': 'male', 'city': 'beijing', 'job': 'coder'}
注意:
- **kw表示把info这个字典以 k-v 关键字的形式传入到函数的 **kw 参数,kw将获取到一个字典,这个字典是对info的一份拷贝,对kw的改动不会影响info的信息。
5. 命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
仍以person()函数为例,我们希望检查是否有city和job参数:
>>> def person(name, age, **kw):
... if 'city' in kw:
... pass
... if 'job' in kw:
... pass
... print('name:', name, 'age:', age, 'other:', kw)
...
>>> person('jason', 25, city='beijing', addr='chaoyang', gender='male')
name: jason age: 25 other: {'city': 'beijing', 'addr': 'chaoyang', 'gender': 'male'}
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符 *,星号后面的参数被视为命名关键字参数。
调用方式如下:
>>> def person(name,age,*,city,job):
... print(name,age,city,job)
...
>>> person('jason',25,city='beijing',job='coder')
jason 25 beijing coder
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
>>> person('jack',25,'beijing','coder')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given
由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。
命名关键字参数可以有缺省值,从而简化调用:
>>> def person(name,age,*,city='beijing',job):
... print(name,age,city,job)
...
>>> person('jason',25,job='coder')
jason 25 beijing coder
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
>>> person('jason',25,job='coder')
jason 25 beijing coder
也可以给默认值city传入参数:
>>> person('jason',25,city='shanghai',job='coder')
jason 25 shanghai coder
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个 * 作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass
6. 参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
>>> def f1(a,b,c=0,*args,**kwargs):
... print('a=',a,'b=',b,'c=',c,'args=',args,'keargs=',kwargs)
...
>>> def f2(a,b,c=2,*,d,**kw):
... print('a=',a,'b=',b,'c=',c,'d=',d,'kw=',kw)
...
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去:
>>> f1(1,2)
a= 1 b= 2 c= 0 args= () keargs= {}
>>>
>>> f1(1,2,c=3)
a= 1 b= 2 c= 3 args= () keargs= {}
>>>
>>> f1(1,2,3,'a','b')
a= 1 b= 2 c= 3 args= ('a', 'b') keargs= {}
>>>
>>> f1(1,2,3,'a','b',x=99)
a= 1 b= 2 c= 3 args= ('a', 'b') keargs= {'x': 99}
>>>
>>> f2(1,2,d=90,ex=None)
a= 1 b= 2 c= 2 d= 90 kw= {'ex': None}
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
>>> args={1,2,3,4}
>>> kw={'d':99,'x':'#'}
>>> f1(*args,**kw)
a= 1 b= 2 c= 3 args= (4,) keargs= {'d': 99, 'x': '#'}
>>>
>>> args = (1,2,3)
>>> kw={'d':88,'x':'@'}
>>> f2(*args, **kw)
a= 1 b= 2 c= 3 d= 88 kw= {'x': '@'}
所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。
说明:
- 虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
demo:创建一个函数接收一个或多个数字,打印这些数字的乘积。
>>> def func(*args):
... result = 1
... for x in args:
... result *= x
... print(result)
...
>>> func(1)
1
>>> func(1,2,3)
6
>>> func(1,2,3,4)
24
>>> func(1,2,3,4,5)
120
小结:
-
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
-
默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
-
要注意定义可变参数和关键字参数的语法:
-
*args是可变参数,args接收的是一个tuple;
-
**kw是关键字参数,kw接收的是一个dict。
-
-
调用函数时如何传入可变参数和关键字参数的语法:
-
可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过args传入:func((1, 2, 3));
-
关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过 ** kw 传入:func(**{‘a’: 1, ‘b’: 2})。
-
-
使用*args和**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
-
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
-
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。
2. 递归函数
1. 概念
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
2. 举例
demo:计算阶乘n! = 1 x 2 x 3 x … x n,用函数func(n)表示:
>>> def func(n):
... if n == 1:
... return 1
... return n * func(n-1)
...
上边就是一个递归函数,测试如下:
>>> func(1)
1
>>> func(2)
2
>>> func(3)
6
>>> func(4)
24
>>> func(5)
120
>>> func(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
3. 优点
定义简单,逻辑清晰。
理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
4. 特点
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试func(1000):
>>> func(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in func
File "<stdin>", line 4, in func
File "<stdin>", line 4, in func
[Previous line repeated 995 more times]
File "<stdin>", line 2, in func
RecursionError: maximum recursion depth exceeded in comparison
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的func(n)函数由于return n * func(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
def func(n):
return func_iter(n, 1)
def func_iter(num, product):
if num == 1:
return product
return func_iter(num - 1, num * product)
可以看到,return func_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
小结:
-
使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
-
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
-
Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
3. 高级特性
1. 切片
- list
>>> name_list = ['Bob','Jason','Tom','Mark','Sarah']
>>> name_list[:3]
['Bob', 'Jason', 'Tom']
>>> name_list[1:3]
['Jason', 'Tom']
>>> name_list[-1]
'Sarah'
- tuple
>>> tuple_num = (1,2,3,4,5,6,7,8)
>>> tuple_num[:5]
(1, 2, 3, 4, 5)
>>> tuple_num[3:]
(4, 5, 6, 7, 8)
>>> tuple_num[:5:2]
(1, 3, 5)
- string
>>> str_text = 'hello world'
>>> str_text[1:5]
'ello'
>>> str_text[1:8]
'ello wo'
>>> str_text[-1:-3]
''
>>> str_text[-3:-1]
'rl'
>>> str_text[-3]
'r'
- set 不支持
>>> set_num = {1,2,3,4,5,6}
>>> set_num[:3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable
- dict 不支持
>>> info = {'name':'Jason','age':25,'gender':'male'}
>>> info[1:2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'slice'
- int 不支持
>>> inin = 12345
>>> inin[1,2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not subscriptable
支持切片的数据类型:列表、元组、字符串
2. 迭代
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
在Python中,迭代是通过for … in来完成的。
list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
>>> dict = {'a':1,'b':2,'c':3,'d':4}
>>> for k in dict:
... print(k)
...
a
b
c
d
所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections
模块的Iterable
类型判断:
>>> from collections import Iterable
>>> isinstance('abc',Iterable)
True
>>> isinstance([1,2,3],Iterable)
True
>>> isinstance({'name':'Jason'}, Iterable)
True
>>> isinstance(123,Iterable)
False
如果要list或tuple打印出带有下标的信息,可以使用 enumerate模块:
>>> for i,v in enumerate(['a','b','c']):
... print(i,v)
...
0 a
1 b
2 c
如果列表中嵌套了列表,每个嵌套的列表含有相同数量的元素,可以这样输出:
>>> for x,y in [(1,2),(3,4),(5,6)]:
... print(x,y)
...
1 2
3 4
5 6
>>> for x,y,z in [(1,2,3),(2,3,4),(3,4,5),('a','b','c')]:
... print(x,y,z)
...
1 2 3
2 3 4
3 4 5
a b c
如果只指定一个变量就跟简单的 for…i…in…list:…print(i)一样:
>>> for x in [(1,2,3),(2,3,4),(3,4,5),('a','b','c')]:
... print(x)
...
(1, 2, 3)
(2, 3, 4)
(3, 4, 5)
('a', 'b', 'c')
3. 列表生成式
- demo1:生成1~11的数字列表:
>>> list(range(1,11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- dem2:生成1~11的平方的数字列表:
>>> [x**2 for x in range(1,11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
- demo3:生成 [AX,AY,AZ,BX,BY,BZ,CX,CY,CZ] 的列表:
>>> [a + b for a in 'ABC' for b in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
- demo4:生成1~10之间偶数的列表:
>>> [x for x in range(1,11) if x % 2 == 0]
[2, 4, 6, 8, 10]
# 因为range()区间,后边的数是不包含的,所以需要写到11才能包含到10
- demo5:列出当前目录下的所有文件和目录名:
>>> import os
>>> [d for d in os.listdir('.')]
['DLLs', 'Doc', 'include', 'Lib', 'libs', 'LICENSE.txt', 'NEWS.txt', 'python.exe', 'python3.dll', 'python37.dll', 'pythonw.exe', 'Scripts', 'tcl', 'Tools', 'vcruntime140.dll']
- demo6:for循环同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:
>>> info = {'name':'Jason','address':'beijing'}
>>> for k,v in info.items():
... print(k + '=' + v)
...
name=Jason
address=beijing
- demo7:把一个list中所有的字符串变成小写:
>>> name_list = ['jason','bob','tom','lili']
>>> [name.capitalize() for name in name_list]
['Jason', 'Bob', 'Tom', 'Lili']
if-else:
- error1:不能在最后的if加上else:
[x for x in range(1, 11) if x % 2 == 0 else 0]
File "<stdin>", line 1
[x for x in range(1, 11) if x % 2 == 0 else 0]
^
SyntaxError: invalid syntax
- error2:if写在for前面,但是不加else会报错:
[x if x % 2 == 0 for x in range(1, 11)]
File "<stdin>", line 1
[x if x % 2 == 0 for x in range(1, 11)]
^
SyntaxError: invalid syntax
其他有意思的事:
-
小计:一行列表表达式中包含 for…if…else的格式:
- [x if 表达式 else 表达式 for x的一个范围]
-
demo1:输出1~10的列表,是2的倍数的则正常输出,不是2的倍数的输出False:
>>> [x if x % 2 == 0 else x == False for x in range(1, 11)]
[True, 2, False, 4, False, 6, False, 8, False, 10]
>>>
# 也可以在是2的倍数的数字中选出一个放到else中:
>>> [x if x % 2 == 0 else x == 2 for x in range(1, 11)]
[False, 2, False, 4, False, 6, False, 8, False, 10]
- demo2:还是输出1~11的列表,是2的倍数的则正常输出,是3输出True,其他输出False:
>>> [x if x % 2 == 0 else x == 3 for x in range(1, 11)]
[False, 2, True, 4, False, 6, False, 8, False, 10]
- demo3:输出1~10的列表,是2的倍数正常输出,不是2的倍数则输出它的相反数:
>>> [x if x % 2 == 0 else -x for x in range(1,11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
- demo4:输出1~10的列表,是2的倍数的正常输出,不是2的倍数的输出指定数字88:
>>> [x if x % 2 == 0 else 88 for x in range(1,11)]
[88, 2, 88, 4, 88, 6, 88, 8, 88, 10]
4. 生成器
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
在Python中,一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的 []
改成 ()
,就创建了一个 generator
:
>>> l = [x for x in range(10)]
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x000001434C9345C8>
创建 l 和 g 的区别仅在于最外层的 []
和 ()
,l 是一个list,而 g 是一个generator。
list直接打印出所有数据,而generator需要使用 next()
函数获得generator的下一个返回值:
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
4
>>> next(g)
5
>>> next(g)
6
>>> next(g)
7
>>> next(g)
8
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration
的错误。
上面这种不断调用next(g)实在是太变态了,正确的方法是使用 for循环
,因为generator也是可迭代对象:
>>> g = (x for x in range(10))
>>> for i in g:
... print(i)
...
0
1
2
3
4
5
6
7
8
9
所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, …
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
Fibonacci.py
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n += 1
print('done')
fib(6)
运行结果:
仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:
def fib2(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
print(fib2(5))
运行结果:
<generator object fib2 at 0x7fc6f8995ba0>
注意:最难理解的就是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)
steo3
5
>>> next(o)
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
next(o)
StopIteration
可以看到,odd不是普通函数,而是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:
>>> def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
>>> for i in fib(6):
print(i)
1
1
2
3
5
8
但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
>>> def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
>>> g = fib(6)
>>> while True:
try:
x = next(g)
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
杨辉三角:
def triangles():
Y = [1] # 初始化为 [1],杨辉三角的每一行都是一个list
while True:
yield Y # yield 实现记录功能,没有下一个next将跳出循环
H = Y[:] # 将 Y 赋值给 H, 通过 H 计算每一行
H.append(0) # 将list添加 0 作为最后一个元素,长度增加 1
Y = [H[i - 1] + H[i] for i in range(len(H))] # 通过 H 计算出 Y
测试:
>>> def triangles():
Y = [1]
while True:
yield Y
H = Y[:]
H.append(0)
Y = [H[i - 1] + H[i] for i in range(len(H))]
>>> g = triangles()
>>> g
<generator object triangles at 0x7fbc0fbea190>
>>> next(g)
[1]
>>> next(g)
[1, 1]
>>> next(g)
[1, 2, 1]
>>> next(g)
[1, 3, 3, 1]
>>> next(g)
[1, 4, 6, 4, 1]
>>> next(g)
[1, 5, 10, 10, 5, 1]
小结:
-
generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
-
要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。
-
对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。
请注意区分普通函数和generator函数:
- 普通函数调用直接返回结果:
>>> r = abs(6)
>>> r
6
- generator函数的“调用”实际返回一个generator对象:
>>> g = fib(6)
>>> g
<generator object fib at 0x1022ef948>
5. 迭代器
直接作用于for循环的数据类型有:
-
一类是集合数据类型:list、tuple、dict、set、string
-
一类是generator,包括生成器和带yield带generator function
可迭代对象Iterable: 可以直接作用于for循环的对象。
可使用 isinstance() 判断一个对象是否是 Iterable 对象:
>>> from collections.abc import Iterable
>>> isinstance([], Iterable) # 列表
True
>>> isinstance({}, Iterable) # 字典
True
>>> isinstance('abc', Iterable) # 字符串
True
>>> isinstance((x for x in range(10)), Iterable) # generator
True
>>> isinstance(list(range(10)), Iterable) # 列表生成式
True
>>> isinstance(100, Iterable) # 整形 不可迭代
False
生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
迭代器 Iterator :可以被next()函数调用并不断返回下一个值的对象。
迭代器有两个基本的方法:iter()
和 next()
。
可以使用isinstance()判断一个对象是否是Iterator对象:
>>> from collections.abc import Iterator
>>> isinstance([], Iterator) # 列表不是迭代器
False
>>> isinstance(list(range(10)), Iterator) # 列表生成器不是迭代器
False
>>> isinstance((x for x in range(10)), Iterator) # generator是迭代器
# a = (x for x in range(10))
# next(a)可以不断生成下一个数字
True
>>> isinstance({}, Iterator) # 字典不是迭代器
False
>>> isinstance('abc', Iterator) # 字符串不是迭代器
False
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
小结:
数据类型 | 生成器 | 可迭代对象 | 迭代器 |
---|---|---|---|
list | 是 | 是 | 否 |
tuple | 是 | 是 | 否 |
dict | 是 | 是 | 否 |
set | 是 | 是 | 否 |
str | 是 | 是 | 否 |
- 可迭代对象可通过 iter()转换成迭代器
为什么list、dict、str等数据类型不是Iterator?
因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
小结:
-
凡是可作用于for循环的对象都是Iterable类型
-
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列
-
集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象
-
Python等for循环本质上就是通过不断调用next()函数实现的,例如:
for i in [1, 2, 3, 4, 5]:
pass
完全等价于:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环
while True:
try:
# 获得下一个值
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
6. 迭代器和生成器总结
1. 迭代器和迭代过程
在Python中,迭代器是遵循迭代协议的对象。
迭代器有两种形式:
- 使用
iter()
从任何序列对象中得到迭代器(如 list、tuple、dict、set等); - 输入迭代器是
generator
(生成器)。
很多容器诸如列表、字符串,可以用for循环遍历对象。for语句会调用容器对象中的 iter()
函数,该函数返回一个定义了 __next__()
方法的迭代器对象,该方法将逐一访问容器中的元素。
Python中的任意对象,只要定义了__next__()方法,它就是一个迭代器。因此,python中的容器如列表、元组、字典、集合、字符串都可以用于创建迭代器。
迭代是从迭代器中取元素的过程。
- demo: 比如用for循环从列表 [1,2,3] 中取元素,这种遍历过程就被称为 迭代。
# 列表、元组、字典、字符串都是迭代器
for i in [1,2,3]: # 列表
print(i)
for i in (1,2,3): # 元组
print(i)
for key in {'one':1,'two':2}: # 字典
print(key)
for i in '123': # 字符串
print(i)
for i in open("myfile.txt"): # 打开的text同样是迭代器
print(i, end='')
如果不想用for循环迭代,可用如下方式:
- 先调用容器的
iter()
函数; - 再使用
next()
内置函数来调用__next__()
方法 - 当元素用尽时,
__next__()
将引发 StopIteration 异常
>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x000002D6514E1788>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
2. 生成器
在Python中,生成器(Generator) 是一边循环一边计算的机制。
生成器也是一种迭代器,但只能迭代一次。因为它们并没有把所有的值存在内存中,而是在运行时生成值。
生成器可通过遍历去使用,用for循环,或将其传递给任意可进行迭代的函数和结构。大多数生成器是以函数来实现的。然而,它们并不返回一个值,而是 yield
(暂且译作 “生出”) 一个值。
每次对生成器调用 next()
时它都会从上次离开位置恢复执行(它会记住上次执行语句时的所有数据值)。
demo:创建生成器
>>> def func(data):
... for i in range(len(data) - 1, -1, -1):
... yield data[i]
...
>>> for char in func('golf'):
... print(char)
...
f
l
o
g
可以用生成器来完成的操作同样可以用基于类的迭代器来完成。但生成器的写法更为紧凑,因为它会自动建 __iter__()
和 __next__()
方法。
3. 生成器表达式
生成器不一定要用复杂的函数表示,Python提供了简洁的生成器表达式。
生成器表达式跟列表推导式很像,仅仅是将列表推导式中的 [] 替换为 () , 但是两者差别挺大,生成器表达式可以说组合了迭代功能和列表解析功能。
生成器表达式可认为是一种特殊的生成器函数,类似于 lambda表达式
和 普通函数
。但是和生成器一样,生成器表达式也是返回生成器generator对象,一次只返回一个值。
>>> sum(i*i for i in range(10)) # 平方和
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x, y in zip(xvec, yvec)) # 点积
260