Python简介
- python语言缺点:①运行速度慢,和C程序相比非常慢,因为Python是解释型语言,你的代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时,所以很慢。而C程序是运行前直接编译成CPU能执行的机器码,所以非常快。②代码不能加密。如果要发布你的Python程序,实际上就是发布源代码,这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是你在Windows上常见的xxx.exe文件)发布出去。要从机器码反推出C代码是不可能的,所以,凡是编译型的语言,都没有这个问题,而解释型的语言,则必须把源码发布出去。
- 在Python交互模式下,可以输入代码,然后执行,并立刻得到结果。在命令行模式下,可以直接运行.py文件。
- input() – 可以让用户输入字符串,并存放到一个变量里
输入/输出 = Input/Output 简写为 I/O
name = input('please enter your name:')
- 1
Python基础
数据类型and编码
- python语言是区分大小写的
- python的除法分两种:① / ② //
/:除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数; // 还有一种除法是//,称为地板除,两个整数的除法仍然是整数: 所以要做精确的除法使用 / 就可以了,还可以使用求取余数的操作,使用 % 就行。
字符集的演变进程:
- 关于字符集的操作:① ord() 函数获取字符的整数表示;② chr() 函数把编码转换为对应的字符;③ 如果知道字符的整数编码,还可以使用十六进制来书写str;
>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
- 1
- 2
- 3
- 4
- 5
- 6
- 在python当中,格式化方式和C语言其实是一致的,都是用 % 来实现
>>> 'Hello, %s' %'World'
'Hello, World'
>>> 'Hi, %s, you have %d' %('HK', 10000)
'Hi, HK, you have 10000'
- 1
- 2
- 3
- 4
占位符 | 替换内容 |
---|---|
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
格式化小数还可以指定小数的位数:
>>> '%.2f' % 3.1415926
'3.14'
- 1
- 2
- 使用format关键字来格式化
>>> 'Hello, {0}成绩提升了{1}'.format('HK',17.125)
'Hello, HK成绩提升了17.125
- 1
- 2
List
- List – python内置的一种数据类型是列表,list是一种有序的集合,可以随时添加和删除其中的元素。
①列出班上几个同学的名字,用一个list来表示
>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']
- 1
- 2
- 3
②使用len() 函数来获取list元素的个数
>>> len(classmates)
3
- 1
- 2
③ 使用索引来访问list中每一个位置的元素,索引是从0开始的
>>> classmates[0]
'Michael'
>>> classmates[1]
'Bob'
>>> classmates[2]
'Tracy'
>>> classmates[3]
报错了,索引出现了越界
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
④ list是一个可变的有序表,可以往list中追加元素到末尾
append
>>> classmates.append('Adam')
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']
- 1
- 2
- 3
⑤ 也可以insert将元素插入到指定的位置
>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
- 1
- 2
- 3
⑥ 删除list末尾的元素 用pop()
>>> classmates.pop()
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']
- 1
- 2
- 3
- 4
⑦ 使用pop(i) 可以删除指定位置的元素
>>> classmates.pop(1)
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']
- 1
- 2
- 3
- 4
⑧ 可以将list中的某个元素替换成别的元素,可以直接赋值给相应的索引位置
>>> classmates[1] = 'Sarah'
>>> classmates
['Michael', 'Sarah', 'Tracy']
- 1
- 2
- 3
⑨ list当中的元素的数据类型可以不同
>>> L = ['Apple', 123, True]
- 1
⑩ list当中也可以包含另一个list
>>> s = ['python', 'java', ['asp', 'php'], 'scheme']
>>> len(s)
4
- 1
- 2
- 3
理解:s只要4个元素,其中s[2]又是一个list,拆开书写更容易理解
>>> p = ['asp', 'php']
>>> s = ['python', 'java', p, 'scheme']
- 1
- 2
要拿到’php’可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
如果一个list中一个元素也没有,就是一个空的list,它的长度为0:
>>> L = []
>>> len(L)
0
- 1
- 2
- 3
元组(Tuple)
另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:
>>> classmates = ('Michael', 'Bob', 'Tracy')
- 1
现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。
不可变tuple的意义:就是因为其不可变所以代码会更安全,如果可以,能用tuple代替list就尽量使用tuple。
注意:
- ① 定义一个tuple时,在定义的时候,tuple元素就必须确定下来。
>>> t = (1, 2)
>>> t
(1, 2)
- 1
- 2
- 3
- ② 定义一个只有一个元素的tuple ,可能会产生歧义
>>> t = (1)
>>> t
1
--- 所以只有一个元素的tuple定义时,必须加一个逗号,以免误解成数学计算意义上的括号
>>> t = (1,)
>>> t
(1,)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- ③ “可变的”的tuple
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])
- 1
- 2
- 3
- 4
- 5
这个tuple定义的时候有3个元素,分别是’a’,’b’和一个list。不是说tuple一旦定义后就不可变了吗?怎么后来又变了?
理解:
先看看定义的时候tuple包含的3个元素:
当我们把list的元素’A’和’B’修改为’X’和’Y’后,tuple变为:
list和tuple是Python内置的有序集合,一个可变,一个不可变。根据需要来选择使用它们。
条件判断
python使用的是缩进原则来实现具体的条件判断:
根据Python的缩进规则,如果if语句判断是True,就把缩进的两行print语句执行了,否则,什么也不做。
age = 3
if age >= 18:
print('adult')
elif age >= 6:
print('teenager')
else:
print('kid')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- if判断条件还可以进行简写操作,例如:
if x:
print('True')
- 1
- 2
只要x是非零数值,非空字符串,非空list等,就可以判断为True,否则就是False
- 再次回顾上次提到的input函数
- 条件判断的形象体现:条件判断从上向下匹配,当满足条件时执行对应的块内语句,后续的elif和else都不再执行。
循环
python循环分为两种:
① 一种是for…in循环,依次把list或tuple中的每个元素迭代出来;
names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)
- 1
- 2
- 3
执行上述代码,会依次打印names中的每一个元素:
Michael
Bob
Tracy
- 1
- 2
- 3
解释:for x in …循环就是把每个元素代入变量x,然后执行缩进块的语句。
实例:计算1-10的整数之和,可以用一个sum变量做累加:
sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
sum = sum + x
print(sum)
- 1
- 2
- 3
- 4
range() 函数,可以生成一个整数序列,在通过list() 函数可以转换为list
>>> list(range(5))
[0, 1, 2, 3, 4]
- 1
- 2
② 另一种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
实例:计算100以内所有奇数之和,可以用while循环实现:
sum = 0
n = 99
while n > 0:
sum = sum + n
n = n - 2
print(sum)
- 1
- 2
- 3
- 4
- 5
- 6
break
使用break 语句可以提前退出循环
n = 1
while n <= 100:
if n > 10: # 当n = 11时,条件满足,执行break语句
break # break语句会结束当前循环
print(n)
n = n + 1
print('END')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解释:打印出1~10后,紧接着打印END,程序结束。可见break的作用是提前结束循环。
continue
使用continue 可以跳过当前这次循环,直接开始下一次循环
实例:在打印过程中,只想打印奇数,可以使用continue语句跳过某些循环
n = 0
while n < 10:
n = n + 1
if n % 2 == 0: # 如果n是偶数,执行continue语句
continue # continue语句会直接继续下一轮循环,后续的print()语句不会执行
print(n)
- 1
- 2
- 3
- 4
- 5
- 6
解释:打印的不再是1~10,而是1,3,5,7,9。
可见continue的作用是提前结束本轮循环,并直接开始下一轮循环。
break和continue会造成代码执行逻辑分叉过多,容易出错。大多数循环并不需要用到break和continue语句。即:break和continue可以少用尽量少用!
dict 和 set
Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
举例:
假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:
names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]
- 1
- 2
很明显,给定一个名字要查找对应的成绩,就先要在names中找到对应的位置,在从scores取出对应的成绩,list越长,耗时越长。
使用dict实现,只需要一个**“名字”-“成绩”**的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95
- 1
- 2
- 3
为什么dict查找速度比list快这么多呢 ?
① 因为dict的实现原理和查字典是一样的。假设字典包含了1万个汉字,我们要查某一个字,一个办法是把字典从第一页往后翻,直到找到我们想要的字为止,这种方法就是在list中查找元素的方法,list越大,查找越慢。
② 第二种方法是先在字典的索引表里(比如部首表)查这个字对应的页码,然后直接翻到该页,找到这个字。无论找哪个字,这种查找速度都非常快,不会随着字典大小的增加而变慢。
dict就是第二种实现方式, 给定一个名字,比如’Michael’,dict在内部就可以直接计算出Michael对应的存放成绩的“页码”,也就是95这个数字存放的内存地址,直接取出来,所以速度非常快。
key-value存储方式
在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。
- 把数据放入dict当中,除了初始化指定之外,还可以通过key放入:
>>> d['Adam'] = 67
>>> d['Adam']
67
- 1
- 2
- 3
- 多次对一个key放入value,后面的值会对前面的值进行覆盖
>>> d['Jack'] = 90
>>> d['Jack']
90
>>> d['Jack'] = 88
>>> d['Jack']
88
- 1
- 2
- 3
- 4
- 5
- 6
- 当键(key)不存在的时候,dict就会报错
>>> d['Thomas']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'Thomas'
- 1
- 2
- 3
- 4
- 避免key不存在错误的两种方法,① 通过in 判断key是否存在
>>> 'Thomas' in d
False
- 1
- 2
② 通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:
>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1
- 1
- 2
- 3
- 删除key – pop(key)方法,对应的value也会从dict中删除
>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}
- 1
- 2
- 3
- 4
- list 与 dict 之间的对比
牢记:dict的key必须是不可变对象。因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
- 1
- 2
- 3
- 4
- 5
set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
- 要创建一个set,需要提供一个list作为输入集合
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}
- 1
- 2
- 3
注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。
- 重复元素在set中自动被过滤
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}
- 1
- 2
- 3
- add(key) 方法可以添加元素到set中,可以重复添加,但不会有效果:
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}
- 1
- 2
- 3
- 4
- 5
- 6
- 通过remove(key) 方法可以删除元素
>>> s.remove(4)
>>> s
{1, 2, 3}
- 1
- 2
- 3
- set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}
- 1
- 2
- 3
- 4
- 5
- 6
- set和dict之间的区别:
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。
不可变对象与可变对象
str是不变对象,而list是可变对象。对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']
- 1
- 2
- 3
- 4
而对于不可变对象,比如str,对str进行操作呢:
>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'
- 1
- 2
- 3
- 4
- 5
虽然字符串有个replace()方法,也确实变出了’Abc’,但变量a最后仍是’abc’,应该怎么理解呢?
>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'
- 1
- 2
- 3
- 4
- 5
- 6
解释:
- 使用key-value存储结构的dict在Python中非常有用,选择不可变对象作为key很重要,最常用的key是字符串。
函数
调用函数
函数文档
- 可以 在交互式命令行中通过help(abs)来查看abs函数的帮助信息
>>> help(abs)
Help on built-in function abs in module builtins:
abs(x, /)
Return the absolute value of the argument.
- 1
- 2
- 3
- 4
- 5
- 调用abs函数
>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34
- 1
- 2
- 3
- 4
- 5
- 6
- abs函数使用可能出现的错误:① 传入参数数量不对 ② 传入的参数类型不能被函数所接受
>>> abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)
- 1
- 2
- 3
- 4
>>> abs('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
- 1
- 2
- 3
- 4
- max函数可以接收任意多个参数,并返回最大的那一个
>>> max(1, 2)
2
>>> max(2, 3, 1, -5)
3
- 1
- 2
- 3
- 4
数据类型转换
- int()函数可以把其他数据类型转换为整数
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
- 1
- 2
- 3
- 练习题
定义函数
在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。
- 自定义一个求绝对值的my_abs函数为例:
def my_abs(x):
if x >= 0:
return x
else:
return -x
print(my_abs(-99))
# 99
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
注意点:
- 函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。函数内部通过条件判断和循环可以实现非常复杂的逻辑。
- 没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。
空函数
- 定义一个什么事也不做的空函数,可以用pass语句:
def nop():
pass
- 1
- 2
- pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
if age >= 18:
pass
- 1
- 2
缺少了pass,代码运行就会有语法错误。
参数检查
- 调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError:
>>> my_abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 2 were given
- 1
- 2
- 3
- 4
- 如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs和内置函数abs的差别:
>>> my_abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
解释:当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,会导致if语句出错,出错信息和abs不一样。所以,这个函数定义不够完善。
- 修改函数my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数,数据类型检查可以用内置函数isinstance() 实现:
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
>>> my_abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in my_abs
TypeError: bad operand type
- 1
- 2
- 3
- 4
- 5
后续还会接着复习到 错误和异常。
返回多个值
应用:比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的坐标:
import math
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
- 1
- 2
- 3
- 4
- 5
- 6
- import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。
- 我们可以同时获取返回值
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
- 1
- 2
- 3
- 但是,Python函数返回的仍然是单一值
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
- 1
- 2
- 3
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
小结
- 定义函数时,需要确定函数名和参数个数;
- 如果有必要,可以先对参数的数据类型做检查;
- 函数体内部可以用return随时返回函数结果;
- 函数执行完毕也没有return语句时,自动return None。
- 函数可以同时返回多个值,但其实就是一个tuple。
函数的参数
- 定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
- Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
位置参数
- 先写一个计算x2的函数
def power(x):
return x * x
- 1
- 2
- 调用power函数时,必须传入有且仅有的一个参数x:
>>> power(5)
25
>>> power(15)
225
- 1
- 2
- 3
- 4
- 把power(x)修改为power(x, n),用来计算xn,说干就干:
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
- 1
- 2
- 3
- 4
- 5
- 6
这个修改后的power(x, n)函数,可以计算任意n次方:
>>> power(5, 2)
25
>>> power(5, 3)
125
- 1
- 2
- 3
- 4
修改后的power(x, n)函数有两个参数:x和n,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x和n。
默认参数
- 由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
- 1
- 2
- 3
- 4
- 5
- 6
- 当我们调用power(5)时,相当于调用power(5, 2):
>>> power(5)
25
>>> power(5, 2)
25
- 1
- 2
- 3
- 4
设置默认参数注意点
- 必选参数在前,默认参数在后,否则Python的解释器会报错
- 如何设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数的好处
使用默认参数最大的好处就是:能降低调用函数的难度。因为在有些情境当中,有一些参数是固定的,不用每次在调用函数的时候都进行传入。
例子:在注册的时候,大多数学生在注册时不需要提供年龄和城市,只提供必须的两个参数:
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
- 1
- 2
- 3
- 4
- 5
默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。
- 有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll(‘Bob’, ‘M’, 7),意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。
- 不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll(‘Adam’, ‘M’, city=‘Tianjin’),意思是,city参数用传进去的值,其他默认参数继续使用默认值.
注意点
定义默认参数要牢记一点:默认参数必须指向不变对象!
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
- 1
- 2
- 3
- 4
- 5
None是不变对象,无论调用多少次,都不会有问题:
>>> add_end()
['END']
>>> add_end()
['END']
- 1
- 2
- 3
- 4
- 为什么要设计str、None这样的不变对象呢?
因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
- 可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
- 实例:给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
- 1
- 2
- 3
- 4
- 5
- 调用的时候,需要先组装出一个list或tuple
>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84
- 1
- 2
- 3
- 4
- 利用可变参数,调用函数的方法可以简化这样:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
- 1
- 2
- 3
- 4
- 把函数的参数改为可变参数:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
- 1
- 2
- 3
- 4
- 5
- 定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
>>> calc(1, 2)
5
>>> calc()
0
- 1
- 2
- 3
- 4
- Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
- 1
- 2
- 3
解释:***nums表示把nums这个list的所有元素作为可变参数传进去。**这种写法相当有用,而且很常见。
关键字参数
- 可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
- 1
- 2
- 函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数
>>> person('Michael', 30)
name: Michael age: 30 other: {}
- 1
- 2
- 也可以传入任意个数的关键字参数
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
- 1
- 2
- 3
- 4
-
关键字参数的作用:它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
-
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
- 1
- 2
- 3
- 将上面的写法进行简化
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
- 1
- 2
- 3
解释:extra表示把extra这个dict的所有key-value用关键字参数传入到函数的kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
命名关键字参数
- 对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
- 以person()函数为例,我们希望检查是否有city和job参数:
def person(name, age, **kw):
if 'city' in kw:
# 有city参数
pass
if 'job' in kw:
# 有job参数
pass
print('name:', name, 'age:', age, 'other:', kw)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
调用者仍可以传入不受限制的关键字参数
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
- 1
!!!如果要限制关键字参数的名字,就可以使用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job):
print(name, age, city, job)
- 1
- 2
- 和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
- 调用方式:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
- 1
- 2
- 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
- 1
- 2
- 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
- 1
- 2
- 3
- 4
解释:由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
- 命名关键字可以有缺省值,从而来简化调用
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
- 1
- 2
上面就是由于,命名关键字参数city具有默认值,调用时,可不传入city参数
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
- 1
- 2
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个作为特殊分隔符。如果缺少,Python解释器将无法识别位置参数和命名关键字参数:
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass
- 1
- 2
- 3
参数组合
- 在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
- 比如定义一个函数,包含上述若干种参数
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
- 1
- 2
- 3
- 4
- 5
- 在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 最神奇的是通过一个tuple和dict,你也可以调用上述函数:
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
!!!对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。
- 虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
练习
小结
递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
实例:我们来计算阶乘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)用递归的方式写出来
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
- 1
- 2
- 3
- 4
实验结果
>>> fact(1)
1
>>> fact(5)
120
- 1
- 2
- 3
- 4
计算过程
递归函数的优点:定义简单,逻辑清晰。其实所有递归函数都可以写成循环方式,但是循环逻辑没有递归那么清晰。
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解决递归调用栈溢出的方法:通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归:在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
– 上面的fact(n)函数由于return n * fact(n – 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解释:return fact_iter(num – 1, num * product)仅返回递归函数本身,num – 1和num * product在函数调用前就会被计算,不影响函数调用。
递归函数调用过程:
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
小结:
- 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
- 针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
- Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
练习
高级特性
切片
- 取一个list或tuple的部分元素是非常常见的操作, 经常取指定索引范围的操作,用循环十分繁琐,Python提供了切片(Slice)操作符,能大大简化这种操作。取前3个元素,用一行代码就可以完成切片:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
- 1
- 2
- 3
解释:L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。
- 如果第一个索引是0,还可以省略:
>>> L[:3]
['Michael', 'Sarah', 'Tracy']
- 1
- 2
- 也可以从索引1开始,取出2个元素出来:
>>> L[1:3]
['Sarah', 'Tracy']
- 1
- 2
- 既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片,倒数第一个元素的索引是-1。
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
- 1
- 2
- 3
- 4
- 切片操作十分有用,先创建一个0-99的数列:
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
- 1
- 2
- 3
- 可以通过切片轻松取出某一段数列,比如①前10个数;②后10个数;③前11-20个数;④前10个数,每两个取一个;⑤所有数,每5个取一个 ⑥只写[:]就可以原样复制一个list;
>>> 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]
>>> L[:10:2]
[0, 2, 4, 6, 8]
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
>>> L[:]
[0, 1, 2, 3, ..., 99]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:– tuple切片操作的结果仍然是tuple
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
- 1
- 2
- 字符串可以看一种list,每个元素就是一个字符,因此字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
- 1
- 2
- 3
- 4
迭代
概念:如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
- Python的for循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b
- 1
- 2
- 3
- 4
- 5
- 6
- 7
-
dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。
-
字符串也是可迭代对象,也可以作用于for循环:当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。
>>> for ch in 'ABC':
... print(ch)
...
A
B
C
- 1
- 2
- 3
- 4
- 5
- 6
- 如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
- 1
- 2
- 3
- 4
- 5
- 6
- for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码。
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9
- 1
- 2
- 3
- 4
- 5
- 6
练习
- 小总结:在使用遍历求取最小或者最大值的时候,可以用变量来保存数组中的第一个值,往后依次做对比即可。
列表生成式
- 列表生成式是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]
- 1
- 2
- 例子2:要生成[1×1, 2×2, 3×3, …, 10×10]怎么做?
方法一:循环
>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
- 1
- 2
- 3
- 4
- 5
- 6
方法二:列表生成式:一行语句代替循环生成上面的list
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
- 1
- 2
小总结:写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。
- for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
- 1
- 2
- 还可以使用两层循环,可以生成全排列
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
- 1
- 2
- 列出当前目录下的所有文件和目录名,可以通过一行代码实现:
>>> import os # 导入os模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
- 1
- 2
- 3
- 前面讲到:for循环其实可以同时使用两个甚至多个变量,比如dict的items可以同时迭代key和value;dict是用{}符号来表示:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
... print(k, '=', v)
...
y = B
x = A
z = C
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 换一种写法,其实就是列表生成式也可以使用两个变量来生成list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
- 1
- 2
- 3
- 把一个list中所有的字符串变成小写:
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
- 1
- 2
- 3
- 在列表生成式当中进行条件的筛选,使用if, 但是不能在if后面加上else,因为只是用来条件的筛选
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
- 1
- 2
- 但是在有些时候,必须在if后面加上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
- 1
- 2
- 3
- 4
- 5
解释:这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else:
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
- 1
- 2
小总结:在一个列表生成式中,for前面的if … else是表达式,而for后面的if是过滤条件,不能带else。
练习
小总结:运用列表生成式,可以快速生成list,可以通过一个list推导出另一个list,而代码却十分简洁。
生成器
- 通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
- 如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器: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>
- 1
- 2
- 3
- 4
- 5
- 6
- 创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generato;
- 可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
可以使用next() 函数来获取生成器的下一个返回值
>>> 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)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。正确的方法是使用for循环,因为generator也是可迭代对象:
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 我们创建了一个generator后,通过for循环来迭代它,并且不需要关心StopIteration的错误。generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
用函数来进行实现
- 实例1:斐波拉契数列
著名的斐波拉契数列(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'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 注意赋值语句
a, b = b, a + b
- 1
解释:不必显式写出临时变量t就可以赋值
t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]
- 1
- 2
- 3
上面的函数可以输出斐波那契数列的前N个数:
>>> fib(6)
1
1
2
3
5
8
'done'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 可以看出,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'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
- 1
- 2
- 3
对于generator函数的解释: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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 调用该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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
解释:
- 可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
- 注意点:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。
- 对生成器函数进行调用的正确写法:创建一个generator对象,然后不断对这一个generator对象调用next():
>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3
>>> next(g)
step 3
5
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 在fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
- 把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
>>> 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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
– 如何捕获错误,后面还会复习到
练习 – 试写一个generator,来打印出一个杨辉三角,见 迭代器打印杨辉三角
小总结
- generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
- 要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。对于函数改成的generator来说**,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束**。
- 普通函数和generator函数的区分
1、普通函数调用直接返回结果:
>>> r = abs(6)
>>> r
6
- 1
- 2
- 3
2、generator函数的调用实际返回一个generator对象:
>>> g = fib(6)
>>> g
<generator object fib at 0x1022ef948>
- 1
- 2
- 3
迭代器
- 直接作用于for循环的数据类型
1、一类是集合数据类型,如list、tuple、dict、set、str等
2、一类是generator,包括生成器和带yield的generator function。 - 上面这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
- 可以使用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)
True
>>> isinstance(100, Iterable)
False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
- 把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
- 1
- 2
- 3
- 4
- 为什么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 x in [1, 2, 3, 4, 5]:
pass
- 1
- 2
实际上面完全等价于:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
try:
# 获得下一个值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
函数式编程
高阶函数
map/reduce
1、map()
- Python内建了map()和reduce()函数。
- map – map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
- 举个实际例子来进行说明
- 使用Python代码来进行实现
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
- 1
- 2
- 3
- 4
- 5
- 6
解释:map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
- 不需要map() 函数,写一个循环,也可以计算出结果
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
L.append(f(n))
print(L)
- 1
- 2
- 3
- 4
- 虽然可以,但是从上面的循环代码,不能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”
- map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
- 1
- 2
2、reduce()
- reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
- 1
- 实例:对一个序列进行求和操作,可以直接使用reduce来实现
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
- 1
- 2
- 3
- 4
- 5
- 6
- 求和运算当然可以直接使用Python内建函数sum(), 没有必要使用reduce
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
- 1
- 2
- 3
- 4
- 5
- 6
- map和reduce用处甚少,这里就不过多赘述。
filter
Python内建的filter()函数用于过滤序列。
- 和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
- 例如:在一个list中,删掉偶数, 只保留奇数
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]
- 1
- 2
- 3
- 4
- 5
– 只有函数最终结果为True的时候,才会保留下来
- 例如:把一个序列中的空字符串删掉:可以这么写:
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
# 结果: ['A', 'B', 'C']
- 1
- 2
- 3
- 4
- 5
- Python当中的strip方法
- filter()这个高阶函数,关键在于正确实现一个**“筛选”**函数。
- filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。
使用filter来求解素数的实例:
- 使用Python来实现这个算法,可以先构造一个从3开始的奇数序列,
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
- 1
- 2
- 3
- 4
- 5
- 上面是一个生成器,并且是一个无限序列,使用filter来定义一个筛选函数:
def _not_divisible(n):
return lambda x: x % n > 0
- 1
- 2
- 定义一个生成器,不断返回下一个素数:
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解释:这个生成器先返回第一个素数2, 然后利用filter() 不断产生筛选后的新的序列
- 而上述primes() 也是一个无限序列,所以调用时需设置一个退出循环的条件:
# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
- 1
- 2
- 3
- 4
- 5
- 6
练习
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:
思路:利用回数的性质, 从左向右读和从右向左读都是一样的数,我们可以利用字符串的性质 外加 切片来进行比较,因为filter是需要写出一个判断函数。
小总结:filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。
排序算法(sorted)
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
- Python内置的sorted()函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
- 1
- 2
- sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
- 1
- 2
- key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:
list = [36, 5, -12, 9, -21]
keys = [36, 5, 12, 9, 21]
- 1
- 2
不需要过多解读,其实就是list上的每一个元素都进行了abs运算。
以上是对sorted([36, 5, -12, 9, -21], key=abs)的解释
- 字符串排序的例子
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
- 1
- 2
- 默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。
- 我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
- 直接给sorted函数传入key函数即可,即可实现忽略大小写的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
- 1
- 2
- 要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
- 1
- 2
- 高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
小总结:sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数(key函数实现即可)
练习:
解释:跟常规的list表达不一样,sorted函数运行时,参数L是每次传其中一个元素进去,例子中就是把L的元素每一个tuple() 传到key里,所以可以用t[n]表示每个tuple里的元素。
但是我们在对一般list进行取值的时候,一般情况下是下面这种形式:有点类似于二维数组:
>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
>>> L[0]
('Bob', 75)
>>> L[1]
('Adam', 92)
>>> L[0][0]
'Bob'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
对字典中的值如何取:
>>> T = {'kang':1999, 'lei':2000, 'YY':1998}
>>> T['kang']
1999
- 1
- 2
- 3
返回函数
即函数作为返回值
- 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
- 实现一个可变参数的求和,通常情况下,可变参数求和函数的定义:
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax
- 1
- 2
- 3
- 4
- 5
- 如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
- 1
- 2
- 3
- 调用函数f时,才真正计算求和的结果:
>>> f()
25
- 1
- 2
- 在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
- 再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
- 1
- 2
- 3
- 4
但是:f1() 和 f2() 调用结果互不影响!
闭包
- 返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
- 需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
- 你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
- 1
- 2
- 3
- 4
- 5
- 6
全部都是9!原因就在于返回的函数引用了变量 i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9
返回闭包时必须牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量!!!
如果一定要引用循环变量怎么办?
方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
再看看结果就是:
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 缺点是代码较长,可利用lambda函数缩短代码。
nonlocal(非局部的)
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:
def inc():
x = 0
def fn():
# 仅读取x的值:
return x + 1
return fn
f = inc()
print(f()) # 1
print(f()) # 1
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:
def inc():
x = 0
def fn():
x = x + 1 # 报错
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
解释:
x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。
所以只要加上一个简单的声明即可:
def inc():
x = 0
def fn():
# nonlocal x
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
!!! 使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。
练习
问题描述:利用闭包返回一个计数器函数,每次调用它返回递增整数:
方法一:
方法二:
小总结:
- 一个函数可以返回一个计算结果,也可以返回一个函数。
- 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。(不要使用循环变量)
匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便
在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
- 1
- 2
- 通过对比可以看出,匿名函数lambda x: x * x实际上就是:
def f(x):
return x * x
- 1
- 2
- 关键字lambda表示匿名函数,冒号前面的x表示函数参数。
- 匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
- 匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
- 1
- 2
- 3
- 4
- 5
- 同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
- 1
- 2
练习
小总结:
- Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。
装饰器
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
>>> def now():
... print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25
- 1
- 2
- 3
- 4
- 5
- 6
- 函数对象有一个__name__属性,可以拿到函数的名字:
>>> now.__name__
'now'
>>> f.__name__
'now'
- 1
- 2
- 3
- 4
- 假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
- 本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
- 1
- 2
- 3
- 4
- 5
- 观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
print('2015-3-25')
- 1
- 2
- 3
- 调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now()
call now():
2015-3-25
- 1
- 2
- 3
解释:把@log放到now()函数的定义处,相当于执行了语句:
now = log(now)
- 1
解释:由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
上面的例子可能很难理解,举一个实际例子来讲解
其实:当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码。
我们一般写判断一个数是不是素数的操作:
import time
def is_prime(num):
if num < 2:
return False
elif num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True
def prime_nums():
t1 = time.time()
for i in range(2, 10000):
if is_prime(i):
print(i)
t2 = time.time()
print(t2 - t1)
prime_nums()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
使用装饰器
'''
装饰器的使用
慢慢开始引入装饰器这个概念
记录时间
当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码
'''
import time
# 来求取一个函数的总运行时间
# 里面这个参数实际意思就是我等下要运行prime_nums这个函数
def display_time(func):
# wrapper() 表示这个函数需要运行哪一些内容
def wrapper():
# 比如说我们要做的是计时
# 1、先截取一个时间
t1 = time.time()
# 2、运行一下我要走的那个函数
func()
# 3、截取另一段时间
t2 = time.time()
print(t2 - t1)
return wrapper
# 判断一个数字是不是素数
def is_prime(num):
if num < 2:
return False
elif num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True
# 如何使用装饰器,使用 @
@ display_time
def prime_nums():
for i in range(2, 10000):
if is_prime(i):
print(i)
'''
此时运行这个函数的时候会:
1、先运行display_time这个装饰器
2、运行wrapper里面的内容:
3、最后才是prime_nums()这个函数实现运行操作
'''
prime_nums ()
- 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
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
!!! 简单使用装饰器以后和之前那个运行结果其实是一样的。
- 假设接下来我们想要统计2-10000之间有多少个质数?
- 先将上面解释器里面的demo进行小小变动即可
@ display_time
def count_prime_nums():
count = 0
for i in range(2, 10000):
if is_prime(i):
count = count + 1
return count
count = count_prime_nums()
print(count)
count_prime_nums()
0.46373701095581055
None
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
但是上述代码在执行以后,并没有打印出统计结果,那是因为返回值并没有传到func里面:
如果想要返回返回值的话,那么需要在wrapper() 里面将返回值返回出来!!!
即修改以后变成:
def display_time(func):
# wrapper() 表示这个函数需要运行哪一些内容
def wrapper():
# 比如说我们要做的是计时
# 1、先截取一个时间
t1 = time.time()
# 2、运行一下我要走的那个函数
# 将函数返回值记录到一个变量result当中
result = func()
# 3、截取另一段时间
t2 = time.time()
print("Total time: {:.4} s".format(t2 - t1))
return result
return wrapper
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
如果装饰器下面那个函数带了参数呢?比如在这个例子当中,我们不是到10000,而是想看2到任意数字,则代码变成这样:
def count_prime_nums(maxnum):
count = 0
for i in range(2, maxnum):
if is_prime(i):
count = count + 1
return count
count = count_prime_nums(1999)
print(count)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
此时程序会报错:
TypeError: wrapper() takes 0 positional arguments but 1 was given
- 1
那么函数带了参数以后装饰器应该怎么进行修改呢?
同样也是在wrapper里面进行修改:
def display_time(func):
# wrapper() 表示这个函数需要运行哪一些内容
# 如果不知道有多少个参数的话,可以写得稍微笼统一点
def wrapper(*args):
# 比如说我们要做的是计时
# 1、先截取一个时间
t1 = time.time()
# 2、运行一下我要走的那个函数
# 将函数返回值记录到一个变量result当中
result = func(*args)
# 3、截取另一段时间
t2 = time.time()
print("Total time: {:.4} s".format(t2 - t1))
return result
return wrapper
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
最后总的例子实现代码是:
'''
装饰器的使用
慢慢开始引入装饰器这个概念
记录时间
当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码
'''
import time
# 来求取一个函数的总运行时间
# 里面这个参数实际意思就是我等下要运行prime_nums这个函数
def display_time(func):
# wrapper() 表示这个函数需要运行哪一些内容
# 如果不知道有多少个参数的话,可以写得稍微笼统一点
def wrapper(*args):
# 比如说我们要做的是计时
# 1、先截取一个时间
t1 = time.time()
# 2、运行一下我要走的那个函数
# 将函数返回值记录到一个变量result当中
result = func(*args)
# 3、截取另一段时间
t2 = time.time()
print("Total time: {:.4} s".format(t2 - t1))
return result
return wrapper
# 判断一个数字是不是素数
def is_prime(num):
if num < 2:
return False
elif num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True
# 如何使用装饰器,使用 @
@ display_time
# def prime_nums():
# for i in range(2, 10000):
# if is_prime(i):
# print(i)
def count_prime_nums(maxnum):
count = 0
for i in range(2, maxnum):
if is_prime(i):
count = count + 1
return count
count = count_prime_nums(5000)
print(count)
'''
此时运行这个函数的时候会:
1、先运行display_time这个装饰器
2、运行wrapper里面的内容:
3、最后才是prime_nums()这个函数实现运行操作
'''
# count_prime_nums()
- 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
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
运行结果是:
Total time: 0.1388 s
669
- 1
- 2
练习:
请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:
小总结:
- 在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。
- decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。
- 了解 + 使用 = YYDS
偏函数
- Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。Python当中的偏函数和数学意义上的偏函数是不一样的。
- 介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
- 举例:int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345')
12345
- 1
- 2
- int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
- 1
- 2
- 3
- 4
base是可以省略不写的
- 假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2):
return int(x, base)
- 1
- 2
- 此时,转换二进制就非常方便了,只要传入自己想要传的字符串即可
>>> int2('1000000')
64
>>> int2('1010101')
85
- 1
- 2
- 3
- 4
- 简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
>>> int2('1000000', base=10)
1000000
- 1
- 2
- 创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:
int2 = functools.partial(int, base=2)
- 1
- 实际上固定了int()函数的关键字参数base,也就是:
int2('10010')
- 1
相当于:
kw = { 'base': 2 }
int('10010', **kw)
- 1
- 2
当传入:
max2 = functools.partial(max, 10)
- 1
实际上会把10作为*args的一部分自动加到左边,也就是:
max2(5, 6, 7)
- 1
就相当于变成:
args = (10, 5, 6, 7)
max(*args)
- 1
- 2
最终上面的函数的结果是 10 。
小总结:
- 当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
模块
使用模块
- Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。
- 我们以内建的sys模块为例,编写一个hello的模块:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__':
test()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
对上面代码的解释:
-
第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
-
第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
-
第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;
-
以上就是Python模块的标准文件模板,当然也可以全部删掉不写,但是,按标准办事肯定没错。
后面开始就是真正的代码部分。 -
你可能注意到了,使用sys模块的第一步,就是导入该模块:
import sys
- 1
导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。 – 关键
sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:
运行python3 hello.py获得的sys.argv就是[‘hello.py’];
- 最后,注意到这两行代码:
if __name__=='__main__':
test()
- 1
- 2
当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
- 我们可以用命令行运行hello.py看看效果:
$ python3 hello.py
Hello, world!
$ python hello.py Michael
Hello, Michael!
- 1
- 2
- 3
- 4
- 如果启动Python交互环境,再导入hello模块:
$ python3
Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>>
- 1
- 2
- 3
- 4
- 5
- 6
- 导入时,没有打印Hello, word!,因为没有执行test()函数。
- 调用hello.test()时,才能打印出Hello, word!:
>>> hello.test()
Hello, world!
- 1
- 2
作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过**_前缀**来实现的。
- 正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;
- 类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名;
- 类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
- 之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
- private函数或变量不应该被别人引用,那它们有什么用呢?请看例子:
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。– 还是非常关键的!!!
安装第三方模块
方法一:
- 在Python中,安装第三方模块,是通过包管理工具pip完成的。
- 如果你正在使用Mac或Linux,安装pip本身这个步骤就可以跳过了。
- 在命令提示符窗口下(cmd)尝试运行pip,如果Windows提示未找到命令,可以重新运行安装程序添加pip。
- 注意:Mac或Linux上有可能并存Python 3.x和Python 2.x,因此对应的pip命令是pip3。
- 例如,我们要安装一个第三方库——Python Imaging Library,这是Python下非常强大的处理图像的工具库。不过,PIL目前只支持到Python 2.7,并且有年头没有更新了,因此,基于PIL的Pillow项目开发非常活跃,并且支持最新的Python 3。
- 一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者pypi上搜索,比如Pillow的名称叫Pillow,因此,安装Pillow的命令就是:
pip install Pillow
- 1
方法二:
安装常用模块
- 在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。
- 用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。
- 安装好Anaconda后,重新打开命令行窗口,输入python,可以看到Anaconda的信息:
可以尝试直接import numpy等已安装的第三方模块。
模块搜索路径
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错:
>>> import mymodule
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named mymodule
- 1
- 2
- 3
- 4
- 默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:
>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']
- 1
- 2
- 3
如果我们要添加自己的搜索目录,有两种方法:
- 一是直接修改sys.path,添加要搜索的目录:这种方法是在运行时修改,运行结束后失效。
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')
- 1
- 2
- 第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。– 自行百度即可。
面向对象编程
类和实例
- 面向对象最重要的概念就是类(Class)和实例(Instance)
- 必须牢记类是抽象的模板,比如Student类
- 而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
仍以Student类为例,在Python中,定义类是通过class关键字:
class Student(object):
pass
- 1
- 2
解释:class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
- 1
- 2
- 3
- 4
- 5
解释:可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。
- 可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
- 1
- 2
- 3
- 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__即为初始化方法,在创建实例的时候,就把name,score等属性绑上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
- 1
- 2
- 3
- 4
- 5
!!!注意:特殊方法“init”前后分别有两个下划线!!!
- 注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
- 有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
- 1
- 2
- 3
- 4
- 5
- 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:
>>> def print_score(std):
... print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59
- 1
- 2
- 3
- 4
- 5
但是,既然Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入
>>> bart.print_score()
Bart Simpson: 59
- 1
- 2
这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
封装的另一个好处是可以给Student类增加新的方法,比如get_grade:
class Student(object):
...
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
同样的,get_grade方法可以直接在实例变量上调用,不需要知道内部实现细节:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
// 测试
lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())
// run
Lisa A
Bart C
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
小总结:
- 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
- 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
- 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
- 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
访问限制
- 在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
- 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 99
>>> bart.score
99
- 1
- 2
- 3
- 4
- 5
- 6
- 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name和实例变量.__score了:
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
- 1
- 2
- 3
- 4
- 5
- 这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
- 但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
class Student(object):
...
def set_score(self, score):
self.__score = score
- 1
- 2
- 3
- 4
- 5
外部代码想要获取或者修改属性的话,设置相应的get和set方法即可
- 你也许会问,原先那种直接通过bart.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数: – 直接写了一个方法里面对参数进行了判断,选择出符合自己的
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__、__score__这样的变量名。
- 有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
- 双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:
>>> bart._Student__name
'Bart Simpson'
- 1
- 2
- 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。
- 总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
- 最后注意下面的错误写法:
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'
- 1
- 2
- 3
- 4
- 5
- 6
表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:
>>> bart.get_name() # get_name()内部返回self.__name
'Bart Simpson'
- 1
- 2
练习:
请把下面的Student对象的gender字段对外隐藏起来,用get_gender()和set_gender()代替,并检查参数有效性:
继承和多态
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:
class Animal(object):
def run(self):
print('Animal is running...')
- 1
- 2
- 3
- 当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
- 1
- 2
- 3
- 4
- 5
解释:对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。Cat和Dog类似。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:
// 先创建相应的对象
dog = Dog()
dog.run()
cat = Cat()
cat.run()
- 1
- 2
- 3
- 4
- 5
- 6
运行结果如下:
Animal is running...
Animal is running...
- 1
- 2
当然,也可以对子类增加一些方法,比如Dog类:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running…,符合逻辑的做法是分别显示Dog is running…和Cat is running…,因此,对Dog和Cat类改进如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
再次运行,运行结果为:
Dog is running...
Cat is running...
- 1
- 2
当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。
理解多态:
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型
- 1
- 2
- 3
判断一个变量是否是某个类型可以用**isinstance()**判断:
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
- 1
- 2
- 3
- 4
- 5
- 6
看来a、b、c确实对应着list、Animal、Dog这3种类型。但是等等,试试:
>>> isinstance(c, Animal)
True
- 1
- 2
看来c不仅仅是Dog,c还是Animal!
不过仔细想想,这是有道理的,因为**Dog是从Animal继承下来的,**当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种!
- 所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
- 1
- 2
- 3
- Dog可以看成Animal,但Animal不可以看成Dog。
- 要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal):
animal.run()
animal.run()
- 1
- 2
- 3
- 当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
- 1
- 2
- 3
- 当我们传入Dog的实例时,run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
- 1
- 2
- 3
- 当我们传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat is running...
- 1
- 2
- 3
- 看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
- 1
- 2
- 3
- 当我们调用run_twice()时,传入Tortoise的实例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
- 1
- 2
- 3
- 你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
- 多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
- 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:① 对扩展开放:允许新增Animal子类;② 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
- 继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
静态语言 vs 动态语言 - 对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法
- 对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:
class Timer(object):
def run(self):
print('Start...')
- 1
- 2
- 3
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
即要求没有那么严格
**Python的“file-like object“**就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
小总结:
- 继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。
- 动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
获取对象信息
- 当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
可以使用type() - 首先,我们来判断对象类型,使用type()函数:
- 基本类型都可以用type()判断:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
- 1
- 2
- 3
- 4
- 5
- 6
- 如果一个变量指向函数或者是类,也可以使用 type() 判断
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
- 1
- 2
- 3
- 4
- 但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
使用isinstance()
- 对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
- 我们回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky
- 1
- 那么,isinstance()就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
- 1
- 2
- 3
- 然后,使用函数isinstance() 来进行判断即可
>>> isinstance(h, Husky)
True
- 1
- 2
没有问题,因为h变量指向的就是Husky对象。
接着判断:
>>> isinstance(h, Dog)
True
- 1
- 2
解释:
h虽然自身是Husky类型,但由于Husky是从Dog继承下来的,所以,h也还是Dog类型。换句话说,isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
- 因此,我们可以确信,h还是Animal类型:
>>> isinstance(h, Animal)
True
- 1
- 2
- 同理,实际类型是Dog的d也是Animal类型:
>>> isinstance(d, Dog) and isinstance(d, Animal)
True
- 1
- 2
- 但是,d不是Husky类型:
>>> isinstance(d, Husky)
False
- 1
- 2
- 而且, 能用type() 判断的基本类型也可以用isinstance()判断:
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
- 1
- 2
- 3
- 4
- 5
- 6
- 并且还可以判断一个变量是否是某些类型中的一种(是否是某种类型中的一种),比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
- 1
- 2
- 3
- 4
总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。
使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
- 1
- 2
- 类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
- 1
- 2
- 3
- 4
- 我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 剩下的都是普通属性或方法,比如lower()返回小写的字符串:
>>> 'ABC'.lower()
'abc'
- 1
- 2
- 仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 紧接着,可以测试该对象的属性:
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 如果试图获取不存在的属性,会抛出AttributeError的错误:
>>> getattr(obj, 'z') # 获取属性'z'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
- 1
- 2
- 3
- 4
- 可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404
- 1
- 2
- 3
- 也可以获得对象的方法:
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
小总结:
- 通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
sum = obj.x + obj.y
- 1
就不要写:
sum = getattr(obj, 'x') + getattr(obj, 'y')
- 1
- 一个正确的用法的例子是:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
- 1
- 2
- 3
- 4
- 假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
- 请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。
实例属性和类属性
- 由于Python是动态语言,根据类创建的实例可以任意绑定属性。
- 给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
- 1
- 2
- 3
- 4
- 5
- 6
- 但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
class Student(object):
name = 'Student'
- 1
- 2
- 当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
解释:从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
练习:
为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:
可以通过类名来调用属性
小总结
- 实例属性属于各个实例所有,互不干扰;
- 类属性属于类所有,所有实例共享一个属性;
- 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。
面向对象高级编程
略,后续有需要再学
错误、调试和测试
错误处理
在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就可以知道是否有错,以及出错的原因。在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1。
def foo():
r = some_function()
if r==(-1):
return (-1)
# do something
return r
def bar():
r = foo()
if r==(-1):
print('Error')
else:
pass
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。
- 所以高级语言通常都内置了一套try…except…finally…的错误处理机制,Python也不例外。
try
让我们用一个例子来看看try的机制:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。
- 上面的代码在计算10 / 0时会产生一个除法运算错误:
try...
except: division by zero
finally...
END
- 1
- 2
- 3
- 4
- 从输出可以看到,当错误发生时,后续语句print(‘result:’,
r)不会被执行,except由于捕获到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后,程序继续按照流程往下走。 - 如果把除数0改成2,则执行结果如下:
try...
result: 5
finally...
END
- 1
- 2
- 3
- 4
- 由于没有错误发生,所以except语句块不会被执行,但是finally如果有,则一定会被执行(可以没有finally语句)。
- 你还可以猜测,错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。没错,可以有多个except来捕获不同类型的错误:
try:
print('try...')
r = 10 / int('a')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- int()函数可能会抛出ValueError,所以我们用一个except捕获ValueError,用另一个except捕获ZeroDivisionError。
- 此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:、
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')
- 1
- 2
- 3
- 4
- 5
- 6
- 第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。
- Python所有的错误都是从BaseException类派生的。
- 使用try…except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用bar(),bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
finally:
print('finally...')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try…except…finally的麻烦。
调用栈
- 如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py:
# err.py:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
执行结果如下:
$ python3 err.py
Traceback (most recent call last):
File "err.py", line 11, in <module>
main()
File "err.py", line 9, in main
bar('0')
File "err.py", line 6, in bar
return foo(s) * 2
File "err.py", line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
出错的时候一定要分析错误的调用栈信息, 才能定位错误的位置
记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
# err_logging.py
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e)
main()
print('END')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
通过配置,logging还可以把错误记录到日志文件里,方便事后排查。
抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
- 如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:
# err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
执行,可以最后跟踪到我们自己定义的错误:
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
- 最后我们再来看另外一种错误处理方式:
# err_reraise.py
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
bar()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?
- 其实这种错误处理方式不但没病,而且相当常见。**捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,**让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。
- raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型:
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
- 1
- 2
- 3
- 4
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError。
练习:
运行下面的代码,根据异常信息进行分析,定位出错误源头,并修复:
小总结:
- Python内置的try…except…finally用来处理错误十分方便。出错时,会分析错误信息并定位错误发生的代码位置才是最关键的。
- 程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。
调试
程序能一次写完并正常运行的概率很小,基本不超过1%。总会有各种各样的bug需要修正。有的bug很简单,看看错误信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug。
- 第一种方法简单直接粗暴有效,就是**用print()**把可能有问题的变量打印出来看看:
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 执行后在输出中查找打印的变量值:
$ python err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
- 1
- 2
- 3
- 4
- 5
- 用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。
断言
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
$ python err.py
Traceback (most recent call last):
...
AssertionError: n is zero!
- 1
- 2
- 3
- 4
- 程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
- 1
- 2
- 3
- 4
注意:断言的开关“-O”是英文大写字母O,不是数字0。
关闭后,你可以把所有的assert语句当成pass来看。
logging
- 把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:
import logging
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
- 1
- 2
- 3
- 4
- 5
- 6
- logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?
- 别急,在import logging之后添加一行配置再试试:
import logging
logging.basicConfig(level=logging.INFO)
- 1
- 2
- 可以看到输出了:
$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
- 1
- 2
- 3
- 4
- 5
- 6
- 这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
- logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
小总结:
- 写程序最痛苦的事情莫过于调试,程序往往会以你意想不到的流程来运行,你期待执行的语句其实根本没有执行,这时候,就需要调试了。
- 虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。
单元测试
略
文档测试
略
IO编程
略
进程和线程
略