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)
上一篇 2025年7月26日 上午11:01
下一篇 2025年7月26日 上午11:22


相关推荐

  • 调用百度地图 5.0手机报错java.lang.UnsatisfiedLinkError: No implementation found for int

    调用百度地图 5.0手机报错java.lang.UnsatisfiedLinkError: No implementation found for int转载请注明 http blog csdn net seven2729 article details 调用百度地图 nbsp 5 0 手机报错 黑屏并闪退报错日志 java lang UnsatisfiedL Noimplementa baidu platform comjni map commonmemcac

    2026年3月18日
    1
  • 扣子Coze智能体教程:AI工作流搭建及实战应用

    扣子Coze智能体教程:AI工作流搭建及实战应用

    2026年3月14日
    2
  • js定义点击事件_回字四种写法

    js定义点击事件_回字四种写法嵌入式点击按钮脚本模型btn.onclick=function(){}W3C事件写法添加事件:appEventListener(事件名,事件函数,false)btn.addEventLi

    2022年8月5日
    10
  • 学员管理系统(完整版)

    前言:学员管理系统是刚接触python时算是一个比较难的小项目,毕竟第一次接触这样的思维逻辑,不过用心学起来还是很有趣的,发现乐在其中,也就不觉得难了。下面给大家分享一下学员管理系统较为完整的代码(当然其中肯定有很多不足,毕竟是新手,菜鸟小程序猿,还请见谅!多谢!)言归正传:学员管理拆分为一下步骤:首先:先定义存储所有学员信息的大列表students=[]添加学员,代码如下:查…

    2022年4月5日
    85
  • linux中iostat命令_linux运维和网络运维

    linux中iostat命令_linux运维和网络运维Linux系统中的iostat是I/Ostatistics(输入/输出统计)的缩写,iostat工具将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况。同vmstat一样,iostat也有一个弱点,就是它不能对某个进程进行深入分析,仅对系统的整体情况进行分析。……………

    2022年10月6日
    6
  • 增强Spring @CacheEvict实现key模糊匹配清除

    增强Spring @CacheEvict实现key模糊匹配清除系统中集成了 Springcache 使用 CacheEvict 进行缓存清除 CacheEvict 可以清除指定的 key 同时可以指定 allEntries true 清空 namespace 下的所有元素 现在遇到一个问题使用 allEntries true 清空 namespace 的值只能是常量 但是我现在需要将缓存根据租户的唯一 TelnetID 进行分离 这就导致 allEntries true 不能使用了 否则一旦触发清除缓存 将会导致全部的缓存清空 而我只想清空当前租户的缓存 熟悉 re

    2026年3月9日
    4

发表回复

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

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