python教程 — 廖雪峰

python教程 — 廖雪峰复习 python 知识点 python 语言缺点 运行速度慢 和 C 程序相比非常慢 因为 Python 是解释型语言 你的代码在执行时会一行一行地翻译成 CPU 能理解的机器码 这个翻译过程非常耗时 所以很慢 而 C 程序是运行前直接编译成 CPU 能执行的机器码 所以非常快 代码不能加密 如果要发布你的 Python 程序 实际上就是发布源代码 这一点跟 C 语言不同 C 语言不用发布源代码 只需要把编译后的机器码 也就是你在 Windows 上常见的 xxx exe 文件 发布出去 要从机器码反推出 C 代码是不可能的 所以 凡是编译型的语

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:') 

Python基础

数据类型and编码

  • python语言是区分大小写的
  • python的除法分两种:① / ② //

/:除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数; // 还有一种除法是//,称为地板除,两个整数的除法仍然是整数: 所以要做精确的除法使用 / 就可以了,还可以使用求取余数的操作,使用 % 就行。

  • 关于字符集的操作:① ord() 函数获取字符的整数表示;② chr() 函数把编码转换为对应的字符;③ 如果知道字符的整数编码,还可以使用十六进制来书写str;
>>> ord('A') 65 >>> ord('中') 20013 >>> chr(66) 'B' 
  • 在python当中,格式化方式和C语言其实是一致的,都是用 % 来实现
>>> 'Hello, %s' %'World' 'Hello, World' >>> 'Hi, %s, you have %d' %('HK', 10000) 'Hi, HK, you have 10000' 

在这里插入图片描述

占位符 替换内容
%d 整数
%f 浮点数
%s 字符串
%x 十六进制整数

格式化小数还可以指定小数的位数:

>>> '%.2f' % 3. '3.14' 
  • 使用format关键字来格式化
>>> 'Hello, {0}成绩提升了{1}'.format('HK',17.125) 'Hello, HK成绩提升了17.125 

List

  • List – python内置的一种数据类型是列表,list是一种有序的集合,可以随时添加和删除其中的元素。

①列出班上几个同学的名字,用一个list来表示

>>> classmates = ['Michael', 'Bob', 'Tracy'] >>> classmates ['Michael', 'Bob', 'Tracy'] 

②使用len() 函数来获取list元素的个数

>>> len(classmates) 3 

③ 使用索引来访问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 

④ list是一个可变的有序表,可以往list中追加元素到末尾
append

>>> classmates.append('Adam') >>> classmates ['Michael', 'Bob', 'Tracy', 'Adam'] 

⑤ 也可以insert将元素插入到指定的位置

>>> classmates.insert(1, 'Jack') >>> classmates ['Michael', 'Jack', 'Bob', 'Tracy', 'Adam'] 

⑥ 删除list末尾的元素 用pop()

>>> classmates.pop() 'Adam' >>> classmates ['Michael', 'Jack', 'Bob', 'Tracy'] 

⑦ 使用pop(i) 可以删除指定位置的元素

>>> classmates.pop(1) 'Jack' >>> classmates ['Michael', 'Bob', 'Tracy'] 

⑧ 可以将list中的某个元素替换成别的元素,可以直接赋值给相应的索引位置

>>> classmates[1] = 'Sarah' >>> classmates ['Michael', 'Sarah', 'Tracy'] 

⑨ list当中的元素的数据类型可以不同

>>> L = ['Apple', 123, True] 

⑩ list当中也可以包含另一个list

>>> s = ['python', 'java', ['asp', 'php'], 'scheme'] >>> len(s) 4 

理解:s只要4个元素,其中s[2]又是一个list,拆开书写更容易理解

>>> p = ['asp', 'php'] >>> s = ['python', 'java', p, 'scheme'] 

要拿到’php’可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。

如果一个list中一个元素也没有,就是一个空的list,它的长度为0:

>>> L = [] >>> len(L) 0 

元组(Tuple)

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:

>>> classmates = ('Michael', 'Bob', 'Tracy') 

现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

不可变tuple的意义:就是因为其不可变所以代码会更安全,如果可以,能用tuple代替list就尽量使用tuple。

注意

  • ① 定义一个tuple时,在定义的时候,tuple元素就必须确定下来。
>>> t = (1, 2) >>> t (1, 2) 
  • ② 定义一个只有一个元素的tuple ,可能会产生歧义
>>> t = (1) >>> t 1 --- 所以只有一个元素的tuple定义时,必须加一个逗号,以免误解成数学计算意义上的括号 >>> t = (1,) >>> t (1,) 
  • ③ “可变的”的tuple
>>> t = ('a', 'b', ['A', 'B']) >>> t[2][0] = 'X' >>> t[2][1] = 'Y' >>> t ('a', 'b', ['X', 'Y']) 

条件判断

age = 3 if age >= 18: print('adult') elif age >= 6: print('teenager') else: print('kid') 
  • if判断条件还可以进行简写操作,例如:
if x: print('True') 

只要x是非零数值,非空字符串,非空list等,就可以判断为True,否则就是False

  • 再次回顾上次提到的input函数
    在这里插入图片描述
  • 条件判断的形象体现:条件判断从上向下匹配,当满足条件时执行对应的块内语句,后续的elif和else都不再执行。
    在这里插入图片描述

循环

names = ['Michael', 'Bob', 'Tracy'] for name in names: print(name) 

执行上述代码,会依次打印names中的每一个元素:

Michael Bob Tracy 

解释: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) 

range() 函数,可以生成一个整数序列,在通过list() 函数可以转换为list

>>> list(range(5)) [0, 1, 2, 3, 4] 

② 另一种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
实例:计算100以内所有奇数之和,可以用while循环实现:

sum = 0 n = 99 while n > 0: sum = sum + n n = n - 2 print(sum) 

break
使用break 语句可以提前退出循环

n = 1 while n <= 100: if n > 10: # 当n = 11时,条件满足,执行break语句 break # break语句会结束当前循环 print(n) n = n + 1 print('END') 

解释:打印出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) 

dict 和 set

names = ['Michael', 'Bob', 'Tracy'] scores = [95, 75, 85] 

很明显,给定一个名字要查找对应的成绩,就先要在names中找到对应的位置,在从scores取出对应的成绩,list越长,耗时越长

使用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:

>>> d = { 
   'Michael': 95, 'Bob': 75, 'Tracy': 85} >>> d['Michael'] 95 

dict就是第二种实现方式, 给定一个名字,比如’Michael’,dict在内部就可以直接计算出Michael对应的存放成绩的“页码”,也就是95这个数字存放的内存地址,直接取出来,所以速度非常快。

key-value存储方式
在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。

  • 把数据放入dict当中,除了初始化指定之外,还可以通过key放入:
>>> d['Adam'] = 67 >>> d['Adam'] 67 
  • 多次对一个key放入value,后面的值会对前面的值进行覆盖
>>> d['Jack'] = 90 >>> d['Jack'] 90 >>> d['Jack'] = 88 >>> d['Jack'] 88 
  • 当键(key)不存在的时候,dict就会报错
>>> d['Thomas'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'Thomas' 
  • 避免key不存在错误的两种方法,① 通过in 判断key是否存在
>>> 'Thomas' in d False 

② 通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:

>>> d.get('Thomas') >>> d.get('Thomas', -1) -1 
  • 删除key – pop(key)方法,对应的value也会从dict中删除
>>> d.pop('Bob') 75 >>> d { 
   'Michael': 95, 'Tracy': 85} 
  • 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' 

set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

  • 要创建一个set,需要提供一个list作为输入集合
>>> s = set([1, 2, 3]) >>> s { 
   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} 
  • add(key) 方法可以添加元素到set中,可以重复添加,但不会有效果:
>>> s.add(4) >>> s { 
   1, 2, 3, 4} >>> s.add(4) >>> s { 
   1, 2, 3, 4} 
  • 通过remove(key) 方法可以删除元素
>>> s.remove(4) >>> s { 
   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} 
  • 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'] 

而对于不可变对象,比如str,对str进行操作呢:

>>> a = 'abc' >>> a.replace('a', 'A') 'Abc' >>> a 'abc' 

虽然字符串有个replace()方法,也确实变出了’Abc’,但变量a最后仍是’abc’,应该怎么理解呢?

>>> a = 'abc' >>> b = a.replace('a', 'A') >>> b 'Abc' >>> a 'abc' 
  • 使用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. 
  • 调用abs函数
>>> abs(100) 100 >>> abs(-20) 20 >>> abs(12.34) 12.34 
  • abs函数使用可能出现的错误:① 传入参数数量不对 ② 传入的参数类型不能被函数所接受
>>> abs(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: abs() takes exactly one argument (2 given) 
>>> abs('a') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: bad operand type for abs(): 'str' 
  • max函数可以接收任意多个参数,并返回最大的那一个
>>> max(1, 2) 2 >>> max(2, 3, 1, -5) 3 

数据类型转换

  • 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 
  • 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数 >>> a(-1) # 所以也可以通过a调用abs函数 1 
  • 练习题
    在这里插入图片描述

定义函数

在Python中,定义一个函数要使用def语句,依次写出函数名括号括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

  • 自定义一个求绝对值的my_abs函数为例:
def my_abs(x): if x >= 0: return x else: return -x print(my_abs(-99)) # 99 

注意点:

  • 函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。函数内部通过条件判断和循环可以实现非常复杂的逻辑。
  • 没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。

空函数

  • 定义一个什么事也不做的空函数,可以用pass语句:
def nop(): pass 
  • pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来
if age >= 18: pass 

缺少了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 
  • 如果参数类型不对,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' 

解释:当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,会导致if语句出错,出错信息和abs不一样。所以,这个函数定义不够完善。

  • 修改函数my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数,数据类型检查可以用内置函数isinstance() 实现:
    python教程 -- 廖雪峰
def my_abs(x): if not isinstance(x, (int, float)): raise TypeError('bad operand type') if x >= 0: return x else: return -x 
  • 添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
>>> 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 

后续还会接着复习到 错误和异常

返回多个值
应用:比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的坐标:

import math def move(x, y, step, angle=0): nx = x + step * math.cos(angle) ny = y - step * math.sin(angle) return nx, ny 
  • import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。
  • 我们可以同时获取返回值
>>> x, y = move(100, 100, 60, math.pi / 6) >>> print(x, y) 151.632 70.0 
  • 但是,Python函数返回的仍然是单一值
>>> r = move(100, 100, 60, math.pi / 6) >>> print(r) (151.632, 70.0) 

原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
小结

  • 定义函数时,需要确定函数名和参数个数;
  • 如果有必要,可以先对参数的数据类型做检查;
  • 函数体内部可以用return随时返回函数结果;
  • 函数执行完毕也没有return语句时,自动return None。
  • 函数可以同时返回多个值,但其实就是一个tuple。

函数的参数

  • 定义函数的时候,我们把参数的名字位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
  • Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。

位置参数

  • 先写一个计算x2的函数
def power(x): return x * x 
  • 调用power函数时,必须传入有且仅有的一个参数x:
>>> power(5) 25 >>> power(15) 225 
  • 把power(x)修改为power(x, n),用来计算xn,说干就干:
def power(x, n): s = 1 while n > 0: n = n - 1 s = s * x return s 

这个修改后的power(x, n)函数,可以计算任意n次方:

>>> power(5, 2) 25 >>> power(5, 3) 125 

修改后的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 
  • 当我们调用power(5)时,相当于调用power(5, 2):
>>> power(5) 25 >>> power(5, 2) 25 

设置默认参数注意点

  • 必选参数在前,默认参数在后,否则Python的解释器会报错
  • 如何设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数
    使用默认参数的好处
    使用默认参数最大的好处就是:能降低调用函数的难度。因为在有些情境当中,有一些参数是固定的,不用每次在调用函数的时候都进行传入。
    例子:在注册的时候,大多数学生在注册时不需要提供年龄和城市,只提供必须的两个参数:


def enroll(name, gender, age=6, city='Beijing'): print('name:', name) print('gender:', gender) print('age:', age) print('city:', city) 

默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。

  • 有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用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 

None是不变对象,无论调用多少次,都不会有问题:

>>> add_end() ['END'] >>> add_end() ['END'] 
  • 为什么要设计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 
  • 调用的时候,需要先组装出一个list或tuple
>>> calc([1, 2, 3]) 14 >>> calc((1, 3, 5, 7)) 84 
  • 利用可变参数,调用函数的方法可以简化这样:
>>> calc(1, 2, 3) 14 >>> calc(1, 3, 5, 7) 84 
  • 把函数的参数改为可变参数:
def calc(*numbers): sum = 0 for n in numbers: sum = sum + n * n return sum 
  • 定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
>>> calc(1, 2) 5 >>> calc() 0 
  • Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3] >>> calc(*nums) 14 

解释:*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

  • 可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
def person(name, age, kw): print('name:', name, 'age:', age, 'other:', kw) 
  • 函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数
>>> person('Michael', 30) name: Michael age: 30 other: { 
   } 
  • 也可以传入任意个数的关键字参数
>>> 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'} 
  • 关键字参数的作用:它可以扩展函数的功能。比如,在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'} 
  • 将上面的写法进行简化
>>> extra = { 
   'city': 'Beijing', 'job': 'Engineer'} >>> person('Jack', 24, extra) name: Jack age: 24 other: { 
   'city': 'Beijing', 'job': 'Engineer'} 

解释: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) 

调用者仍可以传入不受限制的关键字参数

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=) 

!!!如果要限制关键字参数的名字,就可以使用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job): print(name, age, city, job) 
  • 和关键字参数kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
  • 调用方式:
>>> person('Jack', 24, city='Beijing', job='Engineer') Jack 24 Beijing Engineer 
  • 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
def person(name, age, *args, city, job): print(name, age, args, city, job) 
  • 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错
>>> 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' 

解释:由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。

  • 命名关键字可以有缺省值,从而来简化调用
def person(name, age, *, city='Beijing', job): print(name, age, city, job) 

上面就是由于,命名关键字参数city具有默认值,调用时,可不传入city参数

>>> person('Jack', 24, job='Engineer') Jack 24 Beijing Engineer 

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个作为特殊分隔符。如果缺少,Python解释器将无法识别位置参数和命名关键字参数:

def person(name, age, city, job): # 缺少 *,city和job被视为位置参数 pass 

参数组合

  • 在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) 
  • 在函数调用的时候,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} 
  • 最神奇的是通过一个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': '#'} 

!!!对于任意函数,都可以通过类似func(*args, kw)的形式调用它,无论它的参数是如何定义的。

  • 虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。

练习
在这里插入图片描述
小结
在这里插入图片描述


递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。
fact(n)用递归的方式写出来

def fact(n): if n==1: return 1 return n * fact(n - 1) 

实验结果

>>> fact(1) 1 >>> fact(5) 120 

计算过程
在这里插入图片描述
递归函数的优点:定义简单,逻辑清晰。其实所有递归函数都可以写成循环方式,但是循环逻辑没有递归那么清晰。
在这里插入图片描述


>>> 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 

解决递归调用栈溢出的方法:通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归:在函数返回的时候,调用自身本身,并且,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) 

解释:return fact_iter(num – 1, num * product)仅返回递归函数本身,num – 1和num * product在函数调用前就会被计算,不影响函数调用。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

小结:

  1. 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
  2. 针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
  3. Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

练习
在这里插入图片描述

高级特性

切片

  • 取一个list或tuple的部分元素是非常常见的操作, 经常取指定索引范围的操作,用循环十分繁琐,Python提供了切片(Slice)操作符,能大大简化这种操作。取前3个元素,用一行代码就可以完成切片:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] >>> L[0:3] ['Michael', 'Sarah', 'Tracy'] 

解释:L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。

  • 如果第一个索引是0,还可以省略:
>>> L[:3] ['Michael', 'Sarah', 'Tracy'] 
  • 也可以从索引1开始,取出2个元素出来:
>>> L[1:3] ['Sarah', 'Tracy'] 
  • 既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片,倒数第一个元素的索引是-1
>>> L[-2:] ['Bob', 'Jack'] >>> L[-2:-1] ['Bob'] 
  • 切片操作十分有用,先创建一个0-99的数列:
>>> L = list(range(100)) >>> L [0, 1, 2, 3, ..., 99] 
  • 可以通过切片轻松取出某一段数列,比如前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] 
  • tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:– tuple切片操作的结果仍然是tuple
>>> (0, 1, 2, 3, 4, 5)[:3] (0, 1, 2) 
  • 字符串可以看一种list,每个元素就是一个字符,因此字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3] 'ABC' >>> 'ABCDEFG'[::2] 'ACEG' 

在这里插入图片描述

迭代

概念:如果给定一个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 
  • 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 
  • 如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:
>>> from collections.abc import Iterable >>> isinstance('abc', Iterable) # str是否可迭代 True >>> isinstance([1,2,3], Iterable) # list是否可迭代 True >>> isinstance(123, Iterable) # 整数是否可迭代 False 
  • 如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']): ... print(i, value) ... 0 A 1 B 2 C 
  • for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码。
>>> for x, y in [(1, 1), (2, 4), (3, 9)]: ... print(x, y) ... 1 1 2 4 3 9 

练习
在这里插入图片描述

  • 小总结:在使用遍历求取最小或者最大值的时候,可以用变量来保存数组中的第一个值,往后依次做对比即可。

列表生成式

  • 列表生成式是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] 
  • 例子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] 

方法二:列表生成式:一行语句代替循环生成上面的list

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

小总结:写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。

  • for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
>>> [x * x for x in range(1, 11) if x % 2 == 0] [4, 16, 36, 64, 100] 
  • 还可以使用两层循环,可以生成全排列
>>> [m + n for m in 'ABC' for n in 'XYZ'] ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ'] 
  • 列出当前目录下的所有文件和目录名,可以通过一行代码实现
>>> 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'] 
  • 前面讲到: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 
  • 换一种写法,其实就是列表生成式也可以使用两个变量来生成list:
>>> d = { 
   'x': 'A', 'y': 'B', 'z': 'C' } >>> [k + '=' + v for k, v in d.items()] ['y=B', 'x=A', 'z=C'] 
  • 把一个list中所有的字符串变成小写:
>>> L = ['Hello', 'World', 'IBM', 'Apple'] >>> [s.lower() for s in L] ['hello', 'world', 'ibm', 'apple'] 
  • 在列表生成式当中进行条件的筛选,使用if, 但是不能在if后面加上else,因为只是用来条件的筛选
>>> [x for x in range(1, 11) if x % 2 == 0] [2, 4, 6, 8, 10] 
  • 但是在有些时候,必须在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 

解释:这是因为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] 

小总结:在一个列表生成式中,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> 
  • 创建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 
  • 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 
  • 我们创建了一个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' 
  • 注意赋值语句
a, b = b, a + b 

解释:不必显式写出临时变量t就可以赋值

t = (b, a + b) # t是一个tuple a = t[0] b = t[1] 

上面的函数可以输出斐波那契数列的前N个数:

>>> fib(6) 1 1 2 3 5 8 'done' 
  • 可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥
  • 创建生成器的方法,方法二:要把fib函数变成generator函数,只需要把print(b)改为yield b就可以了:
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 return 'done' 
  • 这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator
>>> f = fib(6) >>> f <generator object fib at 0x104feaaa0> 

对于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) 
  • 调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值
>>> o = odd() >>> next(o) step 1 1 >>> next(o) step 2 3 >>> next(o) step 3 5 >>> next(o) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration 

解释:

  • 可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
  • 注意点:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。
  • 对生成器函数进行调用的正确写法:创建一个generator对象,然后不断对这一个generator对象调用next():
>>> g = odd() >>> next(g) step 1 1 >>> next(g) step 2 3 >>> next(g) step 3 5 
  • 在fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
  • 把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代
>>> for n in fib(6): ... print(n) ... 1 1 2 3 5 8 
  • 用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 

– 如何捕获错误,后面还会复习到

练习 – 试写一个generator,来打印出一个杨辉三角,见 迭代器打印杨辉三角

小总结

  • generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
  • 要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。
  • 普通函数和generator函数的区分
    1、普通函数调用直接返回结果:
>>> r = abs(6) >>> r 6 

2、generator函数的调用实际返回一个generator对象:

>>> g = fib(6) >>> g <generator object fib at 0x1022ef948> 

迭代器

  • 直接作用于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 
  • 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
  • 把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]), Iterator) True >>> isinstance(iter('abc'), Iterator) True 
  • 为什么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 

实际上面完全等价于:

# 首先获得Iterator对象: it = iter([1, 2, 3, 4, 5]) # 循环: while True: try: # 获得下一个值: x = next(it) except StopIteration: # 遇到StopIteration就退出循环 break 

函数式编程

高阶函数

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] 

解释: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) 
  • 虽然可以,但是从上面的循环代码,不能一眼看明白“把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'] 

2、reduce()

  • reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) 
  • 实例:对一个序列进行求和操作,可以直接使用reduce来实现
>>> from functools import reduce >>> def add(x, y): ... return x + y ... >>> reduce(add, [1, 3, 5, 7, 9]) 25 
  • 求和运算当然可以直接使用Python内建函数sum(), 没有必要使用reduce
>>> from functools import reduce >>> def fn(x, y): ... return x * 10 + y ... >>> reduce(fn, [1, 3, 5, 7, 9]) 13579 
  • 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] 

– 只有函数最终结果为True的时候,才会保留下来

  • 例如:把一个序列中的空字符串删掉:可以这么写:
def not_empty(s): return s and s.strip() list(filter(not_empty, ['A', '', 'B', None, 'C', ' '])) # 结果: ['A', 'B', 'C'] 
  • Python当中的strip方法
    在这里插入图片描述
  • filter()这个高阶函数,关键在于正确实现一个“筛选”函数。
  • filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

使用filter来求解素数的实例:
在这里插入图片描述

  • 使用Python来实现这个算法,可以先构造一个从3开始的奇数序列,
def _odd_iter(): n = 1 while True: n = n + 2 yield n 
  • 上面是一个生成器,并且是一个无限序列,使用filter来定义一个筛选函数:
def _not_divisible(n): return lambda x: x % n > 0 
  • 定义一个生成器,不断返回下一个素数:
def primes(): yield 2 it = _odd_iter() # 初始序列 while True: n = next(it) # 返回序列的第一个数 yield n it = filter(_not_divisible(n), it) # 构造新序列 

解释:这个生成器先返回第一个素数2, 然后利用filter() 不断产生筛选后的新的序列

  • 而上述primes() 也是一个无限序列,所以调用时需设置一个退出循环的条件:
# 打印1000以内的素数: for n in primes(): if n < 1000: print(n) else: break 

练习
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:
思路:利用回数的性质, 从左向右读和从右向左读都是一样的数,我们可以利用字符串的性质 外加 切片来进行比较,因为filter是需要写出一个判断函数。
在这里插入图片描述
小总结:filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。



排序算法(sorted)
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

  • Python内置的sorted()函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21]) [-21, -12, 5, 9, 36] 
  • sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs) [5, 9, -12, -21, 36] 
  • key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:
list = [36, 5, -12, 9, -21] keys = [36, 5, 12, 9, 21] 
  • 字符串排序的例子
>>> sorted(['bob', 'about', 'Zoo', 'Credit']) ['Credit', 'Zoo', 'about', 'bob'] 
  • 默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。
  • 我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
  • 直接给sorted函数传入key函数即可,即可实现忽略大小写的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) ['about', 'bob', 'Credit', 'Zoo'] 
  • 要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) ['Zoo', 'Credit', 'bob', 'about'] 
  • 高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

小总结:sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数(key函数实现即可)

但是我们在对一般list进行取值的时候,一般情况下是下面这种形式:有点类似于二维数组:

>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] >>> L[0] ('Bob', 75) >>> L[1] ('Adam', 92) >>> L[0][0] 'Bob' 

对字典中的值如何取:

>>> T = { 
   'kang':1999, 'lei':2000, 'YY':1998} >>> T['kang'] 1999 

返回函数

即函数作为返回值

  • 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
  • 实现一个可变参数的求和,通常情况下,可变参数求和函数的定义:
def calc_sum(*args): ax = 0 for n in args: ax = ax + n return ax 
  • 如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args): def sum(): ax = 0 for n in args: ax = ax + n return ax return sum 
  • 当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9) >>> f <function lazy_sum.<locals>.sum at 0x101c6ed90> 
  • 调用函数f时,才真正计算求和的结果:
>>> f() 25 
  • 在这个例子中,我们在函数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 

但是: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() 
  • 上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
  • 你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:
>>> f1() 9 >>> f2() 9 >>> f3() 9 

全部都是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 

再看看结果就是:

>>> f1, f2, f3 = count() >>> f1() 1 >>> f2() 4 >>> f3() 9 
  • 缺点是代码较长,可利用lambda函数缩短代码。

nonlocal(非局部的)
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:

def inc(): x = 0 def fn(): # 仅读取x的值: return x + 1 return fn f = inc() print(f()) # 1 print(f()) # 1 
  • 但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错
def inc(): x = 0 def fn(): x = x + 1 # 报错 return x return fn f = inc() print(f()) # 1 print(f()) # 2 

解释
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 

!!! 使用闭包时,对外层变量赋值前,需要先使用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] 
  • 通过对比可以看出,匿名函数lambda x: x * x实际上就是:
def f(x): return x * x 
  • 关键字lambda表示匿名函数,冒号前面的x表示函数参数
  • 匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
  • 匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x >>> f <function <lambda> at 0x101c6ef28> >>> f(5) 25 
  • 同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y): return lambda: x * x + y * y 

练习
在这里插入图片描述
小总结:

  • Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数

>>> def now(): ... print('2015-3-25') ... >>> f = now >>> f() 2015-3-25 
  • 函数对象有一个__name__属性,可以拿到函数的名字:
>>> now.__name__ 'now' >>> f.__name__ 'now' 
  • 假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
  • 本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func): def wrapper(*args, kw): print('call %s():' % func.__name__) return func(*args, kw) return wrapper 
  • 观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log def now(): print('2015-3-25') 
  • 调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now() call now(): 2015-3-25 

解释:把@log放到now()函数的定义处,相当于执行了语句:

now = log(now) 

解释:由于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() 

使用装饰器

''' 装饰器的使用 慢慢开始引入装饰器这个概念 记录时间 当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣, 这个时候就可以使用一种叫做装饰器的东西来整理代码 ''' 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 () 

!!! 简单使用装饰器以后和之前那个运行结果其实是一样的。

  • 假设接下来我们想要统计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. None 
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 

如果装饰器下面那个函数带了参数呢?比如在这个例子当中,我们不是到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) 

此时程序会报错:

TypeError: wrapper() takes 0 positional arguments but 1 was given 
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 

最后总的例子实现代码是:

''' 装饰器的使用 慢慢开始引入装饰器这个概念 记录时间 当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣, 这个时候就可以使用一种叫做装饰器的东西来整理代码 ''' 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() 

运行结果是:

Total time: 0.1388 s 669 
  • 在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现
  • decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。
  • 了解 + 使用 = YYDS

偏函数

  • Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。Python当中的偏函数和数学意义上的偏函数是不一样的。
  • 介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
  • 举例:int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345') 12345 
  • int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:
>>> int('12345', base=8) 5349 >>> int('12345', 16) 74565 

base是可以省略不写的

  • 假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2): return int(x, base) 
  • 此时,转换二进制就非常方便了,只要传入自己想要传的字符串即可
>>> int2('') 64 >>> int2('') 85 
  • 简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
>>> int2('', base=10)  
  • 创建偏函数时,实际上可以接收函数对象、*args和kw这3个参数,当传入:
int2 = functools.partial(int, base=2) 
  • 实际上固定了int()函数的关键字参数base,也就是:
int2('10010') 

相当于:

kw = { 
     'base': 2 } int('10010', kw) 

当传入:

max2 = functools.partial(max, 10) 

实际上会把10作为*args的一部分自动加到左边,也就是:

max2(5, 6, 7) 

就相当于变成:

args = (10, 5, 6, 7) max(*args) 

最终上面的函数的结果是 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行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
  • 第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
  • 第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;
  • 以上就是Python模块的标准文件模板,当然也可以全部删掉不写,但是,按标准办事肯定没错。
    后面开始就是真正的代码部分。
  • 你可能注意到了,使用sys模块的第一步,就是导入该模块:
import sys 

导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。 – 关键

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

运行python3 hello.py获得的sys.argv就是[‘hello.py’];

  • 最后,注意到这两行代码:
if __name__=='__main__': test() 

当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

  • 我们可以用命令行运行hello.py看看效果:
$ python3 hello.py Hello, world! $ python hello.py Michael Hello, Michael! 
  • 如果启动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 >>> 
  • 导入时,没有打印Hello, word!,因为没有执行test()函数。
  • 调用hello.test()时,才能打印出Hello, word!:
>>> hello.test() Hello, world! 

作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在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) 

我们在模块里公开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 
  • 在使用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 
  • 默认情况下,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. 一是直接修改sys.path,添加要搜索的目录:这种方法是在运行时修改,运行结束后失效
>>> import sys >>> sys.path.append('/Users/michael/my_py_scripts') 
  1. 第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。– 自行百度即可。

面向对象编程

类和实例

  • 面向对象最重要的概念就是类(Class)和实例(Instance)
  • 必须牢记类是抽象的模板,比如Student类
  • 而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

仍以Student类为例,在Python中,定义类是通过class关键字

class Student(object): pass 

解释:class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:

>>> bart = Student() >>> bart <__main__.Student object at 0x10a67a590> >>> Student <class '__main__.Student'> 

解释:可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。

  • 可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
>>> bart.name = 'Bart Simpson' >>> bart.name 'Bart Simpson' 
  • 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__即为初始化方法,在创建实例的时候,就把name,score等属性绑上去:
class Student(object): def __init__(self, name, score): self.name = name self.score = score 

!!!注意:特殊方法“init”前后分别有两个下划线!!!

  • 注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
  • 有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
>>> bart = Student('Bart Simpson', 59) >>> bart.name 'Bart Simpson' >>> bart.score 59 
  • 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

>>> def print_score(std): ... print('%s: %s' % (std.name, std.score)) ... >>> print_score(bart) Bart Simpson: 59 

但是,既然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)) 

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入

>>> bart.print_score() Bart Simpson: 59 

这样一来,我们从外部看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' 

同样的,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 

小总结:

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  • 和静态语言不同,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' 

访问限制

  • 在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
  • 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:
>>> bart = Student('Bart Simpson', 59) >>> bart.score 59 >>> bart.score = 99 >>> bart.score 99 
  • 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在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)) 
  • 改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__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' 
  • 这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
  • 但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
class Student(object): ... def get_name(self): return self.__name def get_score(self): return self.__score 
  • 如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
class Student(object): ... def set_score(self, score): self.__score = score 

外部代码想要获取或者修改属性的话,设置相应的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') 
  • 需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__、__score__这样的变量名。
  • 有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
  • 双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:
>>> bart._Student__name 'Bart Simpson' 
  • 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。
  • 总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
  • 最后注意下面的错误写法:
>>> bart = Student('Bart Simpson', 59) >>> bart.get_name() 'Bart Simpson' >>> bart.__name = 'New Name' # 设置__name变量! >>> bart.__name 'New Name' 

表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:

>>> bart.get_name() # get_name()内部返回self.__name 'Bart Simpson' 

练习:
请把下面的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...') 
  • 当我们需要编写Dog和Cat类时,就可以直接从Animal类继承
class Dog(Animal): pass class Cat(Animal): pass 
// 先创建相应的对象 dog = Dog() dog.run() cat = Cat() cat.run() 

运行结果如下:

Animal is running... Animal is running... 

当然,也可以对子类增加一些方法,比如Dog类:

class Dog(Animal): def run(self): print('Dog is running...') def eat(self): print('Eating meat...') 

继承的第二个好处需要我们对代码做一点改进。你看到了,无论是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...') 

再次运行,运行结果为:

Dog is running... Cat is running... 

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态

a = list() # a是list类型 b = Animal() # b是Animal类型 c = Dog() # c是Dog类型 

判断一个变量是否是某个类型可以用isinstance()判断:

>>> isinstance(a, list) True >>> isinstance(b, Animal) True >>> isinstance(c, Dog) True 

看来a、b、c确实对应着list、Animal、Dog这3种类型。但是等等,试试:

>>> isinstance(c, Animal) True 
  • 所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行
>>> b = Animal() >>> isinstance(b, Dog) False 
  • Dog可以看成Animal,但Animal不可以看成Dog。
  • 要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal): animal.run() animal.run() 
  • 当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal()) Animal is running... Animal is running... 
  • 当我们传入Dog的实例时,run_twice()就打印出:
>>> run_twice(Dog()) Dog is running... Dog is running... 
  • 当我们传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat()) Cat is running... Cat is running... 
  • 看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
class Tortoise(Animal): def run(self): print('Tortoise is running slowly...') 
  • 当我们调用run_twice()时,传入Tortoise的实例:
>>> run_twice(Tortoise()) Tortoise is running slowly... Tortoise is running slowly... 
  • 你会发现,新增一个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...') 

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'> 
  • 如果一个变量指向函数或者是类,也可以使用 type() 判断
>>> type(abs) <class 'builtin_function_or_method'> >>> type(a) <class '__main__.Animal'> 
  • 但是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 
  • 判断基本数据类型可以直接写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 

使用isinstance()

  • 对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
  • 我们回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky 
  • 那么,isinstance()就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
>>> a = Animal() >>> d = Dog() >>> h = Husky() 
  • 然后,使用函数isinstance() 来进行判断即可
>>> isinstance(h, Husky) True 
>>> isinstance(h, Dog) True 
  • 因此,我们可以确信,h还是Animal类型:
>>> isinstance(h, Animal) True 
  • 同理,实际类型是Dog的d也是Animal类型:
>>> isinstance(d, Dog) and isinstance(d, Animal) True 
  • 但是,d不是Husky类型:
>>> isinstance(d, Husky) False 
  • 而且, 能用type() 判断的基本类型也可以用isinstance()判断:
>>> isinstance('a', str) True >>> isinstance(123, int) True >>> isinstance(b'a', bytes) True 
  • 并且还可以判断一个变量是否是某些类型中的一种(是否是某种类型中的一种),比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple)) True >>> isinstance((1, 2, 3), (list, tuple)) True 

总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

>>> dir('ABC') ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill'] 
  • 类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC') 3 >>> 'ABC'.__len__() 3 
  • 我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
>>> class MyDog(object): ... def __len__(self): ... return 100 ... >>> dog = MyDog() >>> len(dog) 100 
  • 剩下的都是普通属性或方法,比如lower()返回小写的字符串:
>>> 'ABC'.lower() 'abc' 
  • 仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
>>> class MyObject(object): ... def __init__(self): ... self.x = 9 ... def power(self): ... return self.x * self.x ... >>> obj = MyObject() 
  • 紧接着,可以测试该对象的属性:
>>> 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 
  • 如果试图获取不存在的属性,会抛出AttributeError的错误:
>>> getattr(obj, 'z') # 获取属性'z' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'MyObject' object has no attribute 'z' 
  • 可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404 404 
  • 也可以获得对象的方法:
>>> 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 

小总结:

  • 通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
sum = obj.x + obj.y 

就不要写:

sum = getattr(obj, 'x') + getattr(obj, 'y') 
  • 一个正确的用法的例子是:
def readImage(fp): if hasattr(fp, 'read'): return readData(fp) return None 
  • 假设我们希望从文件流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 
  • 但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
class Student(object): name = 'Student' 
  • 当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
>>> 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 

解释:从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

练习:
为了统计学生人数,可以给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 
  • 一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。
  • 所以高级语言通常都内置了一套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') 

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

  • 上面的代码在计算10 / 0时会产生一个除法运算错误:
try... except: division by zero finally... END 
  • 从输出可以看到,当错误发生时,后续语句print(‘result:’,
    r)不会被执行
    ,except由于捕获到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后,程序继续按照流程往下走。
  • 如果把除数0改成2,则执行结果如下:
try... result: 5 finally... END 
  • 由于没有错误发生,所以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') 
  • 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') 
  • Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:、
try: foo() except ValueError as e: print('ValueError') except UnicodeError as e: print('UnicodeError') 
  • 第二个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...') 
  • 也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写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() 

执行结果如下:

$ 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 
  • 出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
    在这里插入图片描述
    出错的时候一定要分析错误的调用栈信息, 才能定位错误的位置

记录错误
如果不捕获错误,自然可以让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') 

同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

$ 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 

通过配置,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') 

执行,可以最后跟踪到我们自己定义的错误:

$ 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 
  • 只有在必要的时候才定义我们自己的错误类型。如果可以选择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() 
  • 在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?
  • 其实这种错误处理方式不但没病,而且相当常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。
  • raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型
try: 10 / 0 except ZeroDivisionError: raise ValueError('input error!') 

只要是合理的转换逻辑就可以,但是,决不应该把一个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() 
  • 执行后在输出中查找打印的变量值:
$ python err.py >>> n = 0 Traceback (most recent call last): ... ZeroDivisionError: integer division or modulo by zero 
  • 用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。

断言
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代

def foo(s): n = int(s) assert n != 0, 'n is zero!' return 10 / n def main(): foo('0') 

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

如果断言失败,assert语句本身就会抛出AssertionError

$ python err.py Traceback (most recent call last): ... AssertionError: n is zero! 
  • 程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
$ python -O err.py Traceback (most recent call last): ... ZeroDivisionError: division by zero 

注意:断言的开关“-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) 
  • logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?
  • 别急,在import logging之后添加一行配置再试试:
import logging logging.basicConfig(level=logging.INFO) 
  • 可以看到输出了:
$ 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 
  • 这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
  • logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

小总结:

  • 写程序最痛苦的事情莫过于调试,程序往往会以你意想不到的流程来运行,你期待执行的语句其实根本没有执行,这时候,就需要调试了。
  • 虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。

单元测试

文档测试

IO编程

进程和线程

正则表达式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/233365.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号