Python速通复习
多行打印
单引号或者双引号都可以(默认支持转义),r(raw string)也可写在行头
1 | print(''' |
整数除法
当是/的时候输出的是浮点数
当是//的时候只会输出小数点前的数,也就是整数部分
当是%的时候,取余
1 | 10 / 3 |
字符串
对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:
1 | ord('A') |
如果知道字符的整数编码,还可以用十六进制这么写str:
1 | '\u4e2d\u6587' |
要注意区分'ABC'和b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。
以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:
1 | 'ABC'.encode('ascii') |
纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。
模式匹配
简单匹配
1 | age = 15 |
列表匹配
1 | args = ['gcc', 'hello.c', 'world.c'] |
Dict
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
1 | key = [1, 2, 3] |
Set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
要创建一个set,用{x,y,z,...}列出每个元素:
1 | s = {1, 2, 3} |
或者提供一个list作为输入集合:
1 | s = set([1, 2, 3]) |
注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。
重复元素在set中自动被过滤:
1 | s = {1, 1, 2, 2, 3, 3} |
通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:
1 | s.add(4) |
通过remove(key)方法可以删除元素:
1 | s.remove(4) |
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
1 | s1 = {1, 2, 3} |
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。
函数的参数
对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现:
1 | def my_abs(x): |
默认参数
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END再返回:
1 | def add_end(L=[]): |
当你正常调用时,结果似乎不错:
1 | add_end([1, 2, 3]) |
当你使用默认参数调用时,一开始结果也是对的:
1 | add_end() |
但是,再次调用add_end()时,结果就不对了:
1 | add_end() |
很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了'END'后的list。
原因解释如下:
Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
特别注意
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:
1 | def add_end(L=None): |
现在,无论调用多少次,都不会有问题:
1 | add_end() |
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
1 | calc(1, 2, 3) |
我们把函数的参数改为可变参数:
1 | def calc(*numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
1 | calc(1, 2) |
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
1 | nums = [1, 2, 3] |
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
1 | nums = [1, 2, 3] |
*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
关键词参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
1 | def person(name, age, **kw): |
函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:
1 | person('Michael', 30) |
也可以传入任意个数的关键字参数:
1 | person('Bob', 35, city='Beijing') |
关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
1 | extra = {'city': 'Beijing', 'job': 'Engineer'} |
当然,上面复杂的调用可以用简化的写法:
1 | extra = {'city': 'Beijing', 'job': 'Engineer'} |
**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
仍以person()函数为例,我们希望检查是否有city和job参数:
1 | def person(name, age, **kw): |
但是调用者仍可以传入不受限制的关键字参数:
1 | person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456) |
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
调用方式如下:
1 | person('Jack', 24, city='Beijing', job='Engineer') |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | person('Jack', 24, 'Beijing', 'Engineer') |
由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
1 | def person(name, age, *, city='Beijing', job): |
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
1 | person('Jack', 24, job='Engineer') |
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
小结
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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。
递归函数
举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:
fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n
所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。
于是,fact(n)用递归的方式写出来就是:
1 | def fact(n): |
上面就是一个递归函数。可以试试:
1 | fact(1) |
如果我们计算fact(5),可以根据函数定义看到计算过程如下:
1 | => fact(5) |
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000):
1 | fact(1000) |
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
1 | def fact(n): |
可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。
fact(5)对应的fact_iter(5, 1)的调用如下:
1 | => fact_iter(5, 1) |
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
汉诺塔的递归
1 | def move(n, a, b, c): |
1. 规则回顾
-
有 3 根柱子 A(起点)、B(辅助)、C(终点)。
-
有 n 个盘子,从上到下依次变小,在 A 上。
-
一次只能移动一个盘子,并且大盘子不能放在小盘子上。
-
目标:把所有盘子从 A 移动到 C(可借助 B)。
2. 递归思路
move(n, a, b, c)的意思是:
- 将 n 个盘子从柱子 a 借助柱子 b 移动到柱子 c。
分解步骤(递归):
-
将上面 n-1 个盘子从 a 借助 c 移动到 b。
→ 调用
move(n-1, a, c, b) -
将最大的盘子(第 n 个)从 a 直接移动到 c。
→ 打印
a --> c -
将 b 上的 n-1 个盘子借助 a 移动到 c。
→ 调用
move(n-1, b, a, c)
3. 例子 n=3 的执行过程
初始状态:
1 | A: 3(largest) 2(mid) 1(smallest) |
调用:move(3, 'A', 'B', 'C')
步骤 1.1
进入 move(3, 'A', 'B', 'C'):
n=3,执行 else 分支:
- 调用
move(2, 'A', 'C', 'B')将上面 2 个盘子从 A 经过 C 移到 B。
步骤 2.1
进入 move(2, 'A', 'C', 'B'):
n=2,执行 else 分支:
- 调用
move(1, 'A', 'B', 'C')将上面 1 个盘子从 A 经过 B 移到 C。
步骤 3.1
进入 move(1, 'A', 'B', 'C'):
n=1,打印 A --> C
输出 1: A --> C
回到 move(2, 'A', 'C', 'B')的第 1 步完成。
步骤 3.2
move(2, 'A', 'C', 'B')的第 2 步:
打印 A --> B
输出 2: A --> B
步骤 3.3
move(2, 'A', 'C', 'B')的第 3 步:
调用 move(1, 'C', 'A', 'B')将 C 上的 1 个盘子(1号)从 C 经过 A 移到 B。
进入 move(1, 'C', 'A', 'B'):
打印 C --> B
输出 3: C --> B
move(2, 'A', 'C', 'B')结束。
步骤 2.2
回到 move(3, 'A', 'B', 'C')的第 1 步完成,现在上面 2 个盘子在 B 上(顺序是小的在上),A 只剩下最大的 3 号盘。
第 2 步:打印 A --> C
输出 4: A --> C
步骤 2.3
move(3, 'A', 'B', 'C')的第 3 步:
调用 move(2, 'B', 'A', 'C')将 B 上的 2 个盘子经过 A 移到 C。
步骤 4.1
进入 move(2, 'B', 'A', 'C'):
n=2,else 分支:
- 调用
move(1, 'B', 'C', 'A')将 B 上最小的 1 号盘从 B 经过 C 移到 A。
进入 move(1, 'B', 'C', 'A'):
打印 B --> A
输出 5: B --> A
回到 move(2, 'B', 'A', 'C')第 1 步完成。
步骤 4.2
move(2, 'B', 'A', 'C')第 2 步:
打印 B --> C
输出 6: B --> C
步骤 4.3
move(2, 'B', 'A', 'C')第 3 步:
调用 move(1, 'A', 'B', 'C')将 A 上的 1 号盘从 A 移到 C。
进入 move(1, 'A', 'B', 'C'):
打印 A --> C
输出 7: A --> C
move(2, 'B', 'A', 'C')结束,move(3, 'A', 'B', 'C')结束。
4. 最终输出顺序
1 | 1. A --> C |
这样,我们就用递归完成了 3 个盘子从 A 到 C 的移动,且符合“大盘子不能压小盘子”的规则。
这种算法的思维重在推导,我们先从n=2的情况推导,而不用从脑袋先演算n=3的时候。
切片
Python切片的规则始终是 **[start:stop)** 左闭右开区间:
- 包含
start索引的元素 - 不包含
stop索引的元素 - 从
start开始,到stop-1结束
1 | L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] |
前10个数,每两个取一个:
1 | L[:10:2] |
所有数,每5个取一个:
1 | L[::5] |
甚至什么都不写,只写[:]就可以原样复制一个list:
1 | L[:] |
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
1 | (0, 1, 2, 3, 4, 5)[:3] |
字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
1 | 'ABCDEFG'[:3] |
迭代
如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
1 | for i, value in enumerate(['A', 'B', 'C']): |
列表生成式
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)):
1 | list(range(1, 11)) |
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:
1 | L = [] |
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
1 | [x * x for x in range(1, 11)] |
写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
1 | [x * x for x in range(1, 11) if x % 2 == 0] |
还可以使用两层循环,可以生成全排列:
1 | [m + n for m in 'ABC' for n in 'XYZ'] |
三层和三层以上的循环就很少用到了。
运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
1 | import os # 导入os模块,模块的概念后面讲到 |
map
Python内建了map()和reduce()函数。
如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。
我们先看map。map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:
1 | f(x) = x * x |
现在,我们用Python代码实现:
1 | >>> def f(x): |
map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()函数,写一个循环,也可以计算出结果:
1 | L = [] |
的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?
所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
只需要一行代码。
再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比方说对一个序列求和,就可以用reduce实现:
1 | >>> from functools import reduce |
当然求和运算可以直接用Python内建函数sum(),没必要动用reduce。
但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579,reduce就可以派上用场:
1 | >>> from functools import reduce |
这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的函数:
1 | >>> from functools import reduce |
整理成一个str2int的函数就是:
1 | from functools import reduce |
还可以用lambda函数进一步简化成:
1 | from functools import reduce |
也就是说,假设Python没有提供int()函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码!