函数
函数是Python中的一个重要组成部分,合理运用函数可以解决很多问题
函数的基本内容
为了更好的学习函数的相关内容,这里将简单介绍函数,如果已经有一部分基础,可以选择直接跳过这部分
这里就不复杂介绍函数是什么了,简单说就是一个可以重复执行的代码块
创建一个新的函数
如何创建一个函数,其实方法很简单,直接给出例子
1 | def a_function(name): |
在上面的例子中def就是创建函数的语法,而后面的a_function就是这个函数的名字,后面括号内的name是这个函数的一个参数
函数内的return指的是把后面的name返回给调用这个函数的人,这个return语句可以不主动填写,这时候相当于填写了return None
这时候就有人要问了,这个返回给调用这个函数的人是什么意思呢?
我们简单举个例子
1 | def plus_fun(v1, v2): |
可以看到,变量a和变量b作为参数被传进了函数plus_fun里面,其中v1对应的是a,v2对应的是b
而不难发现,print的结果刚好是return后面的v1 + v2的结果
所以,return返回的结果是后面跟着的东西
而这里print语句中,里面使用了plus_fun()函数,所以print语句中输出的东西是这个函数返回的结果
向函数里面传值
我们已经学会了怎么创建一个函数,接下来要知道如何往一个函数里面传参数
这时候就有人要问了,传参数有什么用呢
举一个简单的例子:
这是一个函数,数学意义上的:
$f(x) = x^2$
这里的传参相当于那数字带入这里的x
同时也说明了函数的作用:可以重复执行的代码块,只要传入参数
说回正题,要如何向一个函数传入参数呢?
首先我们要确定能传入多少参数,这由函数后面的括号里面的值的多少决定
以上面的plus_fun()为例,这个函数一共有两个参数,一个为v1 另一个为v2,那么在传入的时候,就只能传入两个参数
而传入参数的方法也很简单,只需要在使用函数的时候在括号里面加上参数即可
1 | def a_fun(list_1, list_2): |
可以看到,传入值的顺序,是与函数定义是后面的参数的顺序相一致的
当然,如果你不想按顺序传入,只需要指定是哪几个值是什么即可:
1 | def a_fun(list_1, list_2): |
可以发现,这里是b先传入,但是由于已经说明了哪个值对应传入的哪几个值,所以输出的结果是不受顺序改变的
默认参数值
这时候就有人要问了,如果要的值是不变的,那怎么办?
解决方法也很简单,只需要在创建函数的时候将值说明即可
1 | def introduct(name,age=18): |
这里可以发现,第一个小明在传入时没有填入age的值,但由于函数在定义时已经将age的值填好了,默认为18,所以不会报错
而第二个例子小华,在传入时重新给了一个值,使得原来函数定义时的值被覆写了,这也说明了在创建函数时写入的值可以被后面使用函数时改变
可变参数
接下来来讲讲可变参数,也就是*args和**kwargs
*args:接收任意数量的位置参数(元组)
**kwargs:接收任意数量的关键字参数(字典)
这里简单举个例子说明
首先是*args
1 | def add_tuple(*args): |
这里可以看到:使用*args可以同时传进多个参数(例如这里的1,2,3,4,5)
同时,还将原本的参数打包成了一个元组(见输出的第一条结果)
这样有什么好处呢?当你的参数数量不确定的时候,便可以直接将所有的参数全部用*args传进函数,让函数自己打包,使得代码更加简洁
此外,*args还可以统一处理传进的参数
1 | def a_func(*args): |
可以看到,这里两个元组都被*args统一传进了函数
还有一个点就是*args的名字时可以改变的,比如想叫*exp也是可以的,但是星号一定要保留,但约定俗成还是统一叫做*args
注意内容
在使用*args的时候还有一些注意的点:
另外一个就是在使用*args的时候要注意位置
下面直接给出例子:
1 | def e_fun(a,b,*args): |
但如果调换了位置的话(b与*args调换),由于*args可以接受任意数量的参数,所以这里会连带把b的值给打包,这就导致了缺少一个参数的值
所以,在定义函数时,可变参数(*arg)要放在位置参数(a, b)之后
接下来是**kwargs
**kwargs是一个特殊的参数,作用是把传进的 关键字参数 打包成字典
什么关键字参数?大概长这样:
1 | def a_func(**kwarys): |
这里引用函数时括号里面的name、age、height、weight就是关键字参数
上面的输出结果如下:
1 | {'name': '李华', 'age': '18', 'height': 170, 'weight': 59} |
可以看到,返回的结果是一个字典
那么这个参数有什么优点呢?
优点在于其可以处理任意数量的关键字参数,不必担心传入时出现多余的参数而无法处理
与上文的*args一样,**kwargs同样支持自定义名称,只要把**保留皆可
另外一个用法是,可以用**解包一个字典
这里给出例子说明:
1 | def a_func(name, age): |
这里相当于把字典person解包成两个参数name和age,之后再传进函数内
注意内容
与上文的*args一样,定义函数时若使用该可变参数同样要注意位置
这里的*kwarys要放到最后面
返回结果与 return 语句
在Python的函数中,如果你想要返回函数的结果,可以使用return语句
在之前的内容中,也已经提及了大量的例子,这些例子中都包含了return语句
接下来来详细介绍一下:
1.无返回值
无返回值也就是不返回任何东西,如果没有填写return语句,则自动视为无返回值
举个例子:
1 | def a_func(): |
这里可以看到,使用print语句打印函数的输出结果是None,也就是无返回值
2.返回单个值
return可以返回很多东西,包括一个值,一条式子等都是可以的
1 | def a_func(a, b): |
接下来是返回一个元组
1 | def a_func(name, age): |
可以看到,这里返回的类型为tuple,也就是元组
接下来是列表和字典:
1 | def l_func(a): |
可以看到,返回的结果分别是列表(list)以及字典(dict)
作用域与闭包
接下来来讲讲作用域和闭包:
作用域
作用域指的是程序中变量、函数和对象的可访问范围。举个鲜明的例子:假设你在自己的房间里面放一个箱子,那么只有在这个房间的人才能用这个箱子,此时便叫做局部作用域,但如果把这个箱子放到公共的地方,那么所有人都能用这个箱子,此时便叫做全局作用域
作用域一共有以下的种类:
| 名称 | 变量位置 |
|---|---|
| 全局作用域(Global) | 位于模块顶部的变量,不在任何函数,类里面 |
| 局部作用域(Local) | 变量位于函数内部,并且只能在该函数里面可见 |
| 嵌套作用域(Enclosing) | 嵌套作用域的出现条件是位于嵌套函数中,此时内部函数可以访问外部函数的变量,注意这里的对象是内部函数而非外部函数 |
| 内置作用域(Built-in) | Python里面内置的函数或者变量 |
下面为每个作用域给出实例:
1 | a = "我位于模块最顶部,是全局作用域" |
注意点
对于全局作用域而言,在当前模块(说白了就是在这个文件里面)下,你可以在任意地方访问,若位于其他模块,则需要使用import语句来导入该变量
对于局部作用域而言,只能在函数内部访问,并且函数结束时变量自动销毁,不可复用
对于嵌套作用域而言,有且仅有内部函数可以访问,如果需要修改外部函数的变量,可以使用nonlocal语句后对闭包作用域(外部函数)的变量进行修改
下面给出实例
1 | def a_func(): |
这里额外再介绍一下Python中查找变量的规则:LEGB规则
L:Local
E:Enclosing
G:Global
B:Built-in
这里的L,也就是Local指的是Python先在函数内部查找变量
而E,指的是Enclosing,也就是在函数内部查找不到变量后便在外部函数里面查找
若还是找不到,那便是G,也就是Global,会在全局作用域里面查找
若以上都没有,则会在B,也就是Bulit-in,在内置作用域里面查找
这就是LEGB规则
闭包
闭包是嵌套在外部函数内部的内部函数,它的特点是在外部函数结束时,作为内部函数中的变量可以被储存起来,不会被销毁
需要注意的是,只有被闭包引用的变量才不会被销毁,而未被引用的变量是会被销毁的,同样的道理,闭包内部的变量也会随着函数的结束而被销毁,除非闭包内部还有另一个函数来引用闭包内部的变量
下面给出例子:
1 | def a_func(): |
在这个例子中,我们可以看到内部函数b_func引用了外部函数a_func的一个变量a,此时内部函数b_func就形成了一个闭包
而在后边的 return b_func 中,这里的函数已经结束,外部函数a_func已经被销毁,但是由于内部函数是个闭包,导致了变量a被保存起来
这也是为什么后面func()可以正常输出a的值的原因
需要注意一点的是,这里内部函数在引用外部函数的变量时不需要加nonlocal,但是如果内部函数出现了修改变量的情况,则需要加nonlocal,否则内部函数会将变量认为是一个新的变量
参数(进阶)
位置参数与关键字参数
在之前的内容已经稍微讲过了有关位置参数和关键字参数的内容,接下来来细致介绍一下相关内容
位置参数和关键字参数都是Python中用来向函数传递参数的一种方式
首先是位置参数
位置参数指的是 按照参数定义的顺序依次传递的参数
具体的特点是:传递参数时按照定义的顺序传递,不可以调换顺序,并且不可以省略,除非有默认值
接下来给出具体例子:
1 | def a_func(name,age,work): |
在上面这个例子中,函数 a_func 被引用时后面带着的三个参数就被称为位置参数,可以看到,三个参数被依次传递到函数内部
这里如果试图调换顺序,则会使得参数的位置错位:
1 | def a_func(name,age,work): |
因此,在使用位置参数时一定要注意参数的传入顺序
接下来是关键字参数,正如这个参数的名字一样,关键字参数可以通过关键词来传递参数,这样的好处是当你在传递参数时,可以不考虑传递的顺序,只需要考虑关键词是否对应即可
下面给出例子:
1 | def a_func(name,age,work): |
可以看到,通过使用关键字参数,即使传递参数时的顺序不是定义函数时的顺序,但每个参数都被正确的传递了,这也体现了关键字参数中不依赖顺序的特点
在使用关键字参数的时候,还可以通过默认值来省略部分参数
下面照例给出例子:
1 | def a_func(name, work, age=20): |
这个例子就是使用了默认值,在函数刚定义的时候将默认值声明皆可
在声明后在引用函数时便可以不导入相对应的值
注意事项
在使用关键字参数的默认值时,要注意声明的时候要把默认值放到参数最后面的位置
例如:
1 | def a_func(name, age = 18 ,work): |
在上面两个函数中,只有第二个函数才是正确的,而第一个函数因为默认值没有放到最后而导致报错
默认参数的陷阱
在上面的关键字参数中,我们已经提到过默认值参数这个概念了,接下来来说说有关默认参数里面的陷阱
当默认参数是可变对象(如列表、字典、集合)时,该对象会在函数定义时被创建,并且所有调用共享同一个实例。
例如,当一个函数默认参数值是一个列表,此时如果调用函数将值加进去会导致不符合目标
1 | def a_func(list_v , list_a = []): |
可以看到,在第二次输出时,并没有按照我们的要求正确输出[2],而是输出了[1,2]
要解决这种情况也很简单,只需要将每次的列表初始化即可,下面给出修正后的例子
1 | def a_func(list_v , list_a = None): |
这里其实还有一点问题,当用户主动传入一个非空列表时,如果按这里的输入,则会导致列表被重置:
1 | def a_func(list_v, list_a = None): |
可以看到,这里的输出并不是我们预想的[100, 200]
这是因为每次调用函数a_func会导致列表重置一次
为了避免这种情况发生,需要在函数里面新增一个判断语句
1 | def a_func(list_v, list_a = None): |
这样就成功避免了传入时原先列表非空的情况
另外还有就是关于默认参数的作用域问题,如果默认参数引用了外部变量,则可能导致在后面更新变量时出现变量不更新的情况
1 | a = 100 |
可以看到,这里在后面更新a的值为200时,后续调用函数并没有改变结果
解包
接下来来讲讲解包,其实这部分的内容已经在可变参数(*args 和 **kwarys)中介绍过了,但还是详细介绍一下
首先是位置参数解包
当使用*时,你可以将一个列表或者元组拆分成多个元素,然后依次到函数的各个位置参数:
1 | def a_func(a1, a2, a3): |
从这里可以看到,列表的三个位置分别被解包为三个值,并被传到了函数里面
如果不使用解包的做法是这样的:
1 | def a_func(a1, a2, a3): |
这里没有使用解包,而是用了传统的列表的项来向函数里面传入值,显然可读性比较低
并且如果列表的数值过多,就会导致需要传入的数值过多,大大影响了代码的可读性
接下来是关键字参数解包
关键字参数解包用于解包字典,之后将解包后的值传进函数,下面直接给出例子
1 | def a_func(name, age): |
可以看到,这里**person将person这个字典解包成对应的内容
由于这里解包之后的参数为关键字参数,所以可以不用在意传入顺序,只需要确保关键字是正确的皆可
接下来是混合解包,说白了就是这两种解包方式可以同时使用:
1 | def a_func(a1, a2, a3, a4, a5): |
可以看到,这里分别使用了位置参数解包以及关键字参数解包
list_1的三个值分别传递给a1,a2,a3,而字典dic因为为关键字参数,所以被分别传递给各自的参数
高阶函数
接下来来讲讲高阶函数,高阶函数并不是指这个函数更加高级,而是指这个函数可以接受其他函数作为参数,或者将函数当做返回值
这样的函数有很多,之后会逐一介绍
首先我们需要理解第一个高阶函数的概念:将函数作为参数
将函数作为参数
正如字面上的意思一致,你可以将一个函数作为一个参数,接下来将用实际例子介绍一下:
1 | def a_func(a1, a2): |
我们来逐一解释这个例子的语句:
首先先从这里看起:print(b_func(a_func,1,2))
这里的可以拆分成几部分,分别是:
- 负责打印的
print() - 负责调用函数的
b_func() - 以及传进去的参数
a_func,1,2
之后我们来看一下调用的这个函数内容是什么:
1 | def b_func(a_func, b1, b2): |
很明显,这里是这样的,返回一个以参数a_func的值的函数,其中这个函数传进了两个值(b1和b2)
需要注意的是,这里函数定义时的参数名a_func与我们导入的函数名相同纯属巧合,你可以随便更改这个函数名:
1 | def b_func(thisisafunc, b1, b2): |
而让我们重新看一下我们传入了什么:很明显是a_func,所以这里就是调用了函数a_func
接下来让我们看看a_func函数的内容:
1 | def a_func(a1, a2): |
很明显,这里是接受两个值:a1和a2,然后返回其相加的值
而这里的a1和a2是是什么?细心往前观察可以看到,就是我们传入的值b1和b2,而这个b1和b2又是什么?再次回看传进函数b_func的值可以发现,b1是1,而b2是2
由此,这里输出的值也就是3了
在理解完高阶函数在将函数作为参数这方面的应用后,我们可以写一个简单的四则计算器:
1 | def plus(p1, p2): |
返回函数
接下来来讲讲返回函数,也就是返回的结果是个函数,这是什么意思呢,直接举个例子分析一下
1 | def a_func(a): |
接下来还是逐一解析语句:
首先看最主要的内容,也就是print(func(3))
这里很明显是往func里面塞一个参数3,诶,那func是什么呢?
往上看我们可以得知:哦,原来func代表的是将参数2传进函数a_func
这时候我们就已经了解到我们的操作是干什么了,接下来来实际看看函数
首先是第一个,也就是函数a_func:
1 | def a_func(a): |
可以看到这个函数里面还有一个函数,我们这里先不考虑,直接看到下面
可以发现返回语句返回的是一个函数,也就是b_func。这里需要注意一点的是,这里函数a_func的参数a已经被我们在一开始调用的时候传入了值(也就是2)
我们看到函数b_func
1 | def b_func(b): |
可以看到,这里的函数内容是返回外部参数a和内部变量b的乘积
这里可能就有人要问了:诶,我刚才看用return语句调用函数的时候没有加括号传进值吗,这不是稳报错吗,你是不是写错了
我们可以回看我们一开始的print语句里面写了什么,也就是:func(3),可以看到这里传进了一个值3,其实这里等价于a_func(2)(3),在调用函数a_func后返回调用的函数b_func时相当于b_func(3)
这里可以看到传入了一个值3,也就是函数b_func里面的位置参数b
所以,这也就是为什么输出结果为6的原因
常见内置高阶函数
在Python中,有很多内置的高阶函数,接下来来逐一介绍
map()
map()函数的作用是返回一个迭代器,具体如下:map(func, iterable)
接下来给出例子:
1 | s = [1,2,3,4,5] |
接下来来逐一解析一下这个语法
首先看一下最主要的函数内容,也就是map(lambda x:x ** 2, s)
按照之前map函数的说明,我们可以看到这里的lambda x:x ** 2是这里的func
而后面的列表s也就是iterable的内容
这里的map(lambda x:x ** 2, s)也就是将列表s里面的值逐一传入前面的匿名函数,并收集函数的返回值,形成一个迭代器
但如果直接print(list_1)就只会得到一个标识信息,用于显示迭代器(也就是map)对象,但不是一个具体的信息
若想要变成一个列表,则需要用list()转换成列表
此外,如果不需要返回全部的列表,可以以下操作(此处涉及到 生成器 的内容)
1 | s = [1,2,3,4,5] |
此处不多说明,有关next()的语法可到生成器的部分阅读
一些小事项
如果你细心观察可以发现,诶,这里似乎也可以用列表推导式来实现map()函数的内容:
1 | s = [1, 2, 3, 4, 5] |
是的,使用列表推导式或者for语句同样可以实现相同的内容,但使用map有一个优势:节省内存
下面给出例子来说明:
1 | import sys |
这里调用了sys库来检测内存使用情况,可以看到,使用map()函数(前提是不转换成列表)只消耗48字节的内存,而其他的方式都消耗了大量的内存
需要注意的一点是,这里如果用 list 将map返回的迭代器转换成列表,消耗的内存是一样的
诶,这里就有人要追问了,那如果消耗的内存是一样的话,那还不是没有用
别急,让我们看一个例子:
1 | s = range(1, 100000) |
可以看到,这两者的输出结果完全正确,但是使用map()所需的内存要更少
这是因为使用列表推导式的时候,需要把所有的数据全列出来,再对列表进行切割
而使用迭代器的时候,只有在对列表取项的时候才会列出数据
这里的话列表推导式总列出数据为99999项,而使用迭代器map()则只列出了10项
这里需要注意的一个点是,迭代器是一次性的,不可重复使用:
1 | s = [1,2,3] |
可以看到这里第二次print(list(a))的时候,输出的是一个空的列表,这说明了迭代器只能使用使用一次
filter
接下来是filter()其格式如下filter(function, iterable)
这个函数的作用是对输入的内容进行筛选,筛选规则是:function,而筛选的内容为:iterable,如果筛选的内容符合筛选的规则,则返回True
下面给出例子:
1 | a = range(20) |
这里可以看到,函数filter()将满足条件的值都提取了出来,这里的条件为这个值除2的余数为0,也就是返回偶数
这是可能就有人要问了,诶,使用filter()用来检测值的方法可以通过if语句来实现,那为什么还要用filter()函数呢
首先,filter()跟map()返回的对象是一致的,都是一个迭代器,这就导致了,当你需要处理大量数据的时候,使用filter()会相较于使用if语句更省内存,当然,同上文map()一样,迭代器都只能使用一次
其他细节点
filter()在设置过滤时,可以设置成None,此时便可以过滤掉假值:
1 | a = [0, 1, 2, 3, 4, 5, "", "0", None, "123", "字符串"] |
可以看到这里输出的字符串中,将所有假值(0,“”,None)都过滤掉了(注:"0"是一个字符串,不是一个假值)
关于假值的补充:
以下内容均为假值:
常量:None、False
数值:0、0.0、0j(复数零)
空序列 / 集合:“”(空字符串)、[](空列表)、()(空元组)、{}(空字典 / 集合)
还有一个点是,filter()返回的值不一定是严格的布尔值,如果返回的结果也可表示为布尔值,那也一样可以
1 | a = [0, 1, 2, 3, 4, 5, -1, -2, -3] |
可以看到,这里0并没有返回到结果中,这是因为0也可以表示为False,所以这里并没有返回,而其他值,由于均不为0(也就是不为False),所以都可以成功返回
reduce
reduce()是Python中的一个内置的高阶函数,其作用是多次调用实现累计的效果,具体用法如下:reduce(function, iterable[, initializer])
这里的function是累计的规则,也就是每次执行什么函数
接下来的iterable是迭代的具体值,后面的initializer是初始值
需要注意的一点是,使用reduce()语句的时候需要导入外部库functools
1 | from functools import reduce |
接下来举一个例子来实际说明:
1 | from functools import reduce |
可以看到,这里的输出结果为列表a的各个值的相乘结果(2 * 3 * 4 * 5 * 6 * 7 * 8 = 40320)
这也说明了reduce()的核心作用是多次迭代
接下来来讲讲加初始值,顾名思义,也就是第一次的时候使用这个值
1 | from functools import reduce |
可以看到,若不加起始值,则结果为35,但是这里加了起始值,所以还要加上起始值
接下来来讲讲一下有意思的操作
由于reduce()可以实现多次调用的效果,所以可以用这个特性实现一些效果
1 | from functools import reduce |
sorted
sorted()的作用是对任何可迭代对象(如列表、元组、字符串)进行排序,返回一个新的已排序列表,原对象保持不变,使用格式如下:sorted(iterable, *, key=None, reverse=False)
接下来来逐一说明一下:
iterable,也就是要排序的内容,后面的*意味着后面的key和reverse必须要用关键字参数,key是指定函数提取出比较的元素,后面的reverse为布尔值,当为False时为升序排列(默认),而为True时就为降序排列
下面给出例子来说明:
1 | a = [2,6,7,12,6,1,4,6,2,4,6,2,8,9,2,1] |
这里给出的例子就是最基本的排序的例子
接下来给出一个使用key提取比较元素的例子:
1 | a = ["directly", "apart","rare","withstand"] |
可以看到,第一个例子是以第一个字母的排列顺序为排序标准,而第二个例子是以第二个字母作为排序标准
使用sorted()排序的对象有很多,可以是列表,元组,甚至是字符串
1 | a = (1, 4, 2, 62, 4, 1, 25, 6, 12, 4) |
可以看到,这里第二个例子的字符串排序标准是先符号,后大写,最后小写
在说明一些例子后,来具体说明一下参数key
key排序的规则有很多,可以使用长度len,绝对值abs或者自定义函数规则
例子可以在上文看到,接下来来说明多级排序
多级排序,顾名思义,可以按照多个规则排序
1 | a = ( |
在上面这个例子中,排序的规则是先以名字首字母为排序标准,而后以年龄为排序标准
若需要翻转排序,直接使用reverse会导致全部翻转,当需要指定一部分翻转的时候,可以如下操作:
1 | a = ([1, 9], [1, 6], [2, 5], [2, 7], [5, 9]) |
可以看到,这里排序的结果是第一个元素按正序排列,而第二个元素按倒序的顺序排列
排列规则还可以用出现的次数来排序:
1 | from collections import Counter |
这里用了Counter()来统计出现的频率,后面你的排序标准先按频率排序,如果频率相同就按出现的字母顺序排序
注意事项
使用sorted()排序的时候,要注意排序的对象不可以混合(例如数字列表里面带了字符串)、
1 | a = [5,3,8,2,"apple"] |
可以看到,这里的报错原因是无法将字符串和整数类型比较
另外,sorted()排序字母的时候默认以大写字母优先:
1 | a = ["Apple","ant","Book","Bunny","Cherry","apple"] |
可以看到,下面输出的结果是优先排列大写字母
那要怎么解决这个情况呢,很简单,排序的时候,在规则key填入将字符串全部转换为小写(.lower())即可:
1 | a = ["Apple","ant","Book","Bunny","Cherry","apple"] |
max & min
max()和min()语句关联性极高,使用方法完全一样,但是输出的结果相反,一个输出最大值,一个输出最小值
具体语法如下:max(iterable, *[, key, default])
照例依次解读:
iterable需要比较的值
后面的*同理,需要用关键词传递
key代表比较的标准
default指的是默认值,如果不填的话,传入的值如果是一个空值,则会导致报错,若填入,并且默认值有填,则返回的值为默认值,不会报错
由于这两者使用的方法一致,所以这里只展示一种
接下来来给出例子
1 | a = [3,2,5,1,72,64,7,5,3,23,7,23,7,2,4,7,32,56,2,3,5,23,3] |
上面是一个最基本的例子,直接遍历找出最大值
接下来给出另一个例子:
1 | a = ["dog", "jump", "red", "flower", "eat", "slow", "star"] |
这里的输出结果为slow的原因是:使用key的时候检测的是第一个字母中字典序最大的字符,也就是s,所以返回的也是slow
除此之外,max比较的对象还可以是元组:
1 | a = [(1,5),(5,2),(3,5),(1,2),(5,5),(5,7),(3,9)] |
这里先比较元组内的第一个值,得出结果是5,而随后会在第一个值为5的元组中比较第二个值,得出结果为7
因此结果为(5, 7)
接下来展示默认值的用法:
1 | a = [] |
此处如果没有写默认值则会报错:
1 | a = [] |
注意事项
max()和min()的注意事项与sorted大同小异,可以直接参照sorted()的内容
zip
zip()同样是Python中的一个内置高阶函数,其用法如下:zip(*iterables)
接下来照例来说明其具体的使用方法
zip()的参数很简单,只有一个*iterables,这时候就有人要说了,诶,这里有星号,所以后面得用iterables=来传递参数
但其实并不是这样的,只有单独将*列出来(例如sorted(iterable, *, key=None, reverse=False))才说明后面的参数需要用到关键字参数
这里的星号作用为 “位置参数收集符”,它的作用是将调用时传入的多个位置参数并将其打包成一个元组
接下来来举个例子说明:
1 | name = ["Zoe", "Jack", "Lily"] |
可以看到,这里的每个列表里面的参数都被收集起来并打包成一个元组
使用zip()打包元组的时候,若打包的数据存在多余,则会自动舍弃:
1 | name = ["Zoe", "Jack", "Lily", "Aliya"] |
可以看到,这里输出的结果还是与上面一样,因为这里age只有三个值,所以自动舍弃掉name多出的值
此外,zip()还可以用于解压数据,接下来给出例子说明:
1 | a = ["apple", "banana", "cherry"] |
在上面这个例子中,先用了zip()打包数据并转换成列表为变量tuple_1,之后再使用zip(*tuple_1)对tuple_1的数据进行解包,item_1和tuple_1分别是接受元素的变量
此外,在循环中,还可以用zip()来遍历多个可迭代的对象:
1 | p = ["Alice","Ethan", "Chloe"] |
这里的原理是,每次for循环都会让变量pe和da分别接收zip()里的数据
而后循环内依次对数据进行引用,引用结束后重新接收数据
zip()还可以将两个列表合并成一个字典:
1 | th = ["name", "age"] |
可以看到,这里输出的结果是一个字典,而这个字典的键就是传入的第一个列表,而对应的值就是第二个传入的列表
注意事项
如果使用zip()遍历的对象是一个字典,此时返回的结果为这个字典的键,而不是值
1 | a = {"name":"Alice","age":23} |
若想要返回的结果为对应的值,则需要在对应的对象后面加上.values():
1 | a = {"name":"Alice","age":23} |
可以看到,这里的结果就为对应的值了
reversed
reversed()的作用是返回一个反向的迭代器,说白了就是把传入的东西反向输出
具体的语法如下:reversed(seq)
这里seq指的是序列,包括列表、元组、字符串、range对象等
也就是说字典是不可以的
接下来给出例子说明:
1 | a = [1,4,5,6,2,1,2,4,4,7,1,3,1,7] |
在给出基础的例子后,接下来分别讲一下各个序列使用后的结果:
1 | a_l = [1,4,7,32,89,3,6,2,4,35] |
一些小技巧
如果需要反转不是序列的值,可以先转换成列表后再对其反转:
1 | d = {"a": 1, "b": 2, "c": 3} |
匿名函数 Lambda
Lambda的作用是创建一个一次性的简单函数,支持传入参数等操作
与普通的函数相比,匿名函数不需要def语句,并且即写即用。如果需要用到一些一次性的语句,便可以使用Lambda语句
接下来来详细介绍Lambda
语法与使用场景
Lambda的语法非常简单,主要由下面三部分组成
1 | lambda x, y : x * y |
这里的lambda是告诉Python说下面的内容是有关匿名函数的内容,而后面的x, y就是这个匿名函数的位置参数,而后面的x * y就是匿名函数的表达式
下面给出一个实际例子:
1 | multiply = lambda x, y : x * y |
在这个例子中,我们定义了一个匿名函数,并通过变量multiply引用它
当然,如果你不想用变量引用,也可以用下面这种形式:
1 | print((lambda x, y: x * y)(1, 2)) |
这样的话代码会比较简洁,但缺点是不能通过变量调用
使用Lambda的场景大部分是在需要使用一些简单的一次性操作中,这类场景由于语句比较简单,所以不需要使用具名函数def来定义函数,故使用匿名函数
与常规函数对比
匿名函数与具名函数的对比很明显,接下来列个表格来说明
| 特性 | 匿名函数lambda | 具名函数def |
|---|---|---|
| 定义方式 | 使用Lambda关键字 | 使用def关键字 |
| 函数名称 | 无函数名称(要不然为什么叫匿名函数),但仍可以用变量引用 | 必须有名称,否则无法调用 |
| 函数主体 | 只能有一条表达式 | 可以用多个表达式,甚至可以有其他函数 |
| 返回结果 | 自动返回 | 用return语句,忘记加自动识别为return None |
| 适合场景 | 简单的语句 | 需要复杂运算(表达式多于一行) |
可以看到,匿名函数与具名函数各有各的特点
接下来用一个具体的例子来说明:
1 | def a_func(a1, a2): |
可以看到在这个例子中,有多个语句的使用了具名函数,而简单计算使用了匿名函数
装饰器
装饰器是Python中一个强大的语法糖,其作用是在不改变函数原有结构的前提下,给函数添加新的功能
其基本结构如下:
1 | def a_func(func): |
给出基本结构后,接下来给出例子:
1 | def a_func(func): |
接下来来逐一讲解一下:
首先,一开始函数定义的时候,便会自动执行以下步骤
1 | m_func = a_func(m_func) |
此时m_func就相当于a_func()返回的值,也就是s_func(),后面调用m_func()实际上是调用s_func()
接下来就是正常执行了,也就是执行m_func(),但实际上是s_func()
这里可能就有人要问了,诶,那我看s_func()里面有个外部参数func()啊,怎么没有传来呢?
其实这里一开始就传进来了,我们可以看到这里,还记得一开始m_func是怎么变成s_func的吗?没错!m_func = a_func(m_func),这里其实已经传进值了,并且由于内部函数s_func()是个闭包,这里的参数没有因为return而被销毁
常见的装饰器
接下来来讲讲Python中常见的装饰器,主要有计时器、日志、权限验证
计时器
首先来讲讲计时器,计时器是装饰器的一个常见的用途,用于记录函数运行的时间,下面给出一个例子来说明:
1 | import time |
接下来来逐一介绍其功能
首先是照例,函数一开始的定义阶段,当执行@timer时(一开始的时候),test1(3,5)转换为check(),顺带把两个参数丢进去闭包里面,之后开始执行check(),所以此时执行的实际上是check(3,5)
check()一开始先记录时间(start_time),之后开始执行函数func()(这里也就是test1())在执行后便再次记录时间(end_time),这里两次计时是为了算出总执行时间
最后打印结果:func.__name__为执行的函数的名字,后面的两个时间相减便是函数执行总耗时,:.4f则为精确到小数点后4位(不取小数点结果则为:0.5001778602600098s)
这里需要另外补充的一点是check(*args, **kwargs)中*args, **kwargs的目的是为了传入所有的值,包括位置参数和关键字参数,这样就可以有效地避免了有参数没传进去,后面的func(*args,**kwargs)则是把传进的数据重新解包出来,然后再传进原函数func()
而一开始的@functools.wraps(func)是为了保存原函数的元数据,也就是test1()的元数据,这里如果不这样做的话,那么使用print(test1.__name__)的结果为check()而不是原先的test1()
日志
通过使用装饰器还可以实现日志的功能:
1 | import functools |
接下来来逐一讲解
首先先讲讲logging.basicConfig()
level=logging.INFO的作用的只让INFO及以上级别的日志能被输出,级别如下:DEBUG < INFO < WARNING < ERROR < CRITICAL
接下来是format='%(asctime)s - %(levelname)s - %(message)s'
这个指的是输出时的默认格式,分别是时间,名称,具体内容
首先还是老样子,在函数定义的时候,a_func(a, b)将参数传给log(func),并且a_func()变为wrapper()
之后开始运行的时候,调用wrapper(),*args和**kwargs一开始定义的时候就被传进去了
接下来来说说wrapper()里面的内容
logging.info,的作用是输出一条INFO级别的信息
带参数的装饰器
接下来讲讲带参数的装饰器
带参数的装饰器也叫做装饰器工厂,作用是通过传递参数来自定义装饰器的行为
接下来来用一个例子来实际说明:
1 | import functools |
可以看到,这里有两个装饰器,并且装饰器带有参数
接下来来简单讲解一下,因为带了参数的装饰器本质上就比普通装饰器多了一层传进参数
首先第一步定义函数阶段:在定义的时候(也就是@timer(precision=3)的时候),会调用timer(precision=3)把参数传进去,之后返回函数decorate()
接下来返回之后,decorate()会接收原函数add_4(),同时将add_4()替换为decorate(add_4)的返回值,也就是wrapper()。其实这一部分是跟一般的装饰器是一样的
之后运行的时候,调用add_4(10, 2)实际上相当于wrapper(10, 2)
类装饰器
接下来来讲讲类装饰器。类装饰器的核心是通过实现 __call__ 方法,让类的实例可以像函数一样被调用,从而实现装饰逻辑。由于类可以通过__init__的初始化状态来记住一些值,所以可以用在一些场景里面,比如说计数
首先给出一个例子:
1 | class Add: |
首先我们从__init__看起,这里__init__把被装饰的函数对象绑给self.func,其实也就是add_1,之后的self.count = 0则是计数的次数,用于后面的计数
接下来看到__call__,这里先接收传进来的值*args, **kwargs,也就是被装饰函数调用时传入的参数
这里对应的是add_1(i[0],i[1])中的i[0],i[1],这里需要强调的,不要看到有两个就想当然的认为*args和**kwargs一人负责一个,这里全由*args负责,因为都是位置参数,不是关键字参数
接下来看到函数的内部,首先第一行是self.count += 1,也就是将运行次数加一
下一行的result是将原函数的运行结果绑定到这个变量上,对应的内容就是return a + b里面的a + b
而后面的两行就是基础的输出内容,这里不多赘述
这里的话装饰器还是一样,在一开始定义的时候自动执行add_1 = Add(add_1),add_1被替换为Add类的实例
接下来是装饰器工厂类,这个的原理跟上文带参数的装饰器差不多,这里不多赘述
生成器
接下来来讲讲生成器。生成器的作用是逐步生成一个值,通过yield返回值,并暂时停止,利用next()重新激活生成器
接下来通过一个例子来说明生成器的用法:
1 | def a_func(n): |
首先一开始先看到这一行:count_1 = a_func(5)这里可以看到,是将一个值(5)传入函数a_func(),但此时函数并未执行,只是创建了一个生成器对象
之后开始执行next(count_1),也就开始运行函数,这里一开始count为1,满足循环count <= n的条件,所以执行循环内的操作,也就是产出count的值,同时将生成器定格在这里
接下来来到第二个next(count_1),由于这里有next()所以刚才在第一步定格的生成器开始执行,也就是运行yield的下一步:count += 1
在执行后重新循环,由于新的count满足循环条件,所以继续进入循环,还是返回count的值并定格
接下来的三、四、五的道理跟二一样
这时候就有人要说了,诶,既然一开始传入的值是5,那么如果执行第六步会发生什么事情呢
如果选择执行第六次的话,则会报错:StopIteration
这是由于已经跳出循环,生成器无法找到yield来返回值导致的
生成器表达式
接下来来讲讲生成器表达式。生成器表达式可以与列表推导式相类比:
1 | b = [x ** 2 for x in l] # 列表推导式 |
可以看到,这里列表推导式和生成器表达式的在表达式上的区别就是包裹的括号的区别
接下来来通过实例来说明两者之间的不同:
1 | l = list(range(1,4)) |
可以看到,这里两个有着巨大的差距
使用列表推导式只需要一次就可以将整个列表输出出来
而生成器表达式则需要逐步才能输出出来
这也是生成器(作为一种迭代器)的一个特点:惰性输出(用的时候才输出)
这有什么好处呢?假设你有一个无限列表,如果用列表推导式输出,由于列表推导式需要将整个列表一次性全部输出出来,但是这个列表是无限长的,这就导致了永远也无法输出
而生成器表达式因其惰性输出的特点,可以选择性的输出值,在这个场景下就可以正常输出
这里还要补充的一点是,如果选择直接输出生成器(也就是不加(next())返回的对象是一个生成器对象,长这样:
1 | <generator object <genexpr> at 内存地址> |
这也是为什么要用next()输出值的原因
如果觉得使用next()不够方便,可以选择for循环来输出:
1 | l = list(range(1,4)) |
这样的好处是一旦结束会自动停止生成,不会抛出StopIteration
诶,为什么可以这样呢?那是因为for循环自动帮我们完成了next()迭代和StopIteration异常处理,并且当遇到StopIteration的时候自动结束,所以不需要我们自行执行next()
递归函数
递归函数是指在函数内部直接或间接调用自身的函数。
简单来说就是在这个函数内部可以自己调用自己,从而实现循环的一种函数
接下来举个例子来说明一下:
1 | def a_func(m): |
接下来解释一下:
首先先从函数a_func()讲起。这里是定义了一个变量c作为最大值,之后返回内部函数b_func(),也就是执行主要的部分
接下来讲讲内部函数b_func(),可以看到这里定义了两个位置参数,分别是n,也就是传进来的数字(这里为2,来自这里a_func(2)),还有另一个位置参数target,也就是之前的变量c,这里的作用是作为一个边界
首先一开始判断是否超过边界,如果没有,则继续执行,其中的重点是return语句,这里可以发现返回的是这个函数,也就是b_func()这里也就是递归函数的体现
之后便一次一次的执行判断,直到跳出不满足结果(也就是达到边界target)
尾递归优化
接下来讲讲尾递归优化,递归分为两种:普通递归 和 尾递归
首先是普通递归,普通递归指的是在调用后还得处理其他操作:
1 | def a_func(n): |
上面就是一个普通递归的例子,在这里例子中,递归函数时还需要执行其他操作,也就是num * a_func(n)
而尾递归是指除了递归函数外没有其他的操作:
1 | def a_func(a,b = 1): |
可以看到,这里的最后一步为调用自己,没有其他的操作
那为什么要尾递归优化呢?递归最大的问题是,每次在调用递归时都会在内存中创建一个叫栈帧的东西,这个东西是用来存储一些保存在函数中的局部变量等信息
而如果递归的次数过多,则会导致栈帧堆积,造成栈溢出
而尾递归优化能识别出尾递归形式,在递归调用时复用当前栈帧(而不是创建新栈帧)。这样无论递归多少次,栈帧数量都保持不变,从根本上避免了栈溢出。
但很可惜的是Python并不支持尾递归优化,但可以用其他方法来替代,比如循环或手动模拟栈
函数式编程
接下来来讲讲函数式编程,强调将计算视为函数的组合,与一般的命令式编程(强调怎么做)不同,函数式编程强调做什么
纯函数
纯函数是函数式编程的核心所在,其满足两个条件:输出仅由输入决定(也称为确定性)和无副作用
接下来来分别介绍这两个条件是什么
输出仅由输入决定(确定性):也就是说如果输入相同,则输出一定相同
无副作用:也就是说不修改函数外部的状态(比如说全局变量)
具体讲讲什么是副作用
副作用
接下来通过例子来说明什么是副作用:
修改全局变量
1 | a = 0 |
在上面这个例子中,全局变量a在函数中得到了修改,也就是其副作用的体现
修改传入的可变参数
1 | l = [] |
在上面这个例子中,副作用体现在可变参数列表新元素的加入
执行I/O操作
1 | a = 1 |
在上面这个例子中,副作用体现在用print打印变量
输入相同,输出不同
这类情况常见发生在有随机数的函数中:
1 | import random |
这里即使传入的参数a是相同的,但由于随机数的原因,输出的结果是不同的
纯函数的内部可以有局部变量、循环等逻辑,但一定要满足所有的变量都是局部的,以及对输入参数的处理是只读的
不可变数据
不可变数据是函数式编程中的一个重点,其基本原理是数据一旦创建,就不能被修改。任何对数据的 “修改” 操作,实际上都会生成一个全新的数据副本,而原始数据始终保持不变
好处是可以避免很多的因为数据变更导致的问题,同时还可以增强可靠性
常见的不可变数据类型
| 类型 | 示例 |
|---|---|
| 整数 | a = 1 |
| 浮点数 | a = 1.1 |
| 字符串 | a = "这是一个字符串" |
| 元组 | a = (1,2,3) |
| 冻结集合 | a = frozenset([1,2]) |
| 布尔值 | a = True |
不可变性的“不可修改”
这里来讲讲不可修改
在看完上文的不可变类型数据之后,这时候可能就有人有疑惑了,诶,那平时写的时候不是一个变量的值可以多次切换吗
1 | a = 1 |
确实,上面的例子中变量a从1切换到了2,但事实上这里是创建了一个新的数据,而不是在原有的基础上修改
魔术方法
魔术方法是一类以__开头和结尾的方法,可以为Python对象定义特定的行为
这些魔术方法的核心特点是:一般情况下不需要手动调用,而是在特定的常见下由解释器自动触发
接下来给出例子来说明:
1 | class Aclass: |
接下来来说明这个例子中的各个部分
基础的__init__作用是创建实例时自动调用,为实例绑定属性。括号内跟着的第一个是参数名,也就是这个实例本身,用来访问这个实例的属性
这里将两个属性分别指定为两个传进来的参数值
之后是另一个魔术方法,也就是__str__
这个魔术方法的作用是,当定义的对象被str()或print()调用时输出的格式
从上面的例子的输出可以看到,内容正好是__str__内return后跟着的格式
魔术方法实现了高度的自定义,可以用这个方法实现诸多的内容
当然,Python中的魔术方法还有很多,接下来列出一些常用的
[TODO]
上下文管理器
接下来来讲讲上下文管理器,也就是__enter__和__exit__
其中__enter__的作用是 进入with代码块的时候自动调用,并且返回的对象会被as吸收
而__exit__则是在退出with代码块的时候自动调用,即使是发生异常也照样调用,主要用于关闭文件、释放资源等操作
接下来举个例子来说明实际用法
1 | import time |
接下来来逐一讲解。首先是with语句,执行的时候便触发__enter__开始计时,之后执行其内部的内容,也就是time.sleep(0.5)
在执行后,with语句结束,执行__exit__,结束计时,并且输出运行时间
调试与测试
函数文档字符串
接下来来讲讲函数文档字符串,这个字符串是一个特殊的字符串,用于注明函数内的内容,也就是解释函数中的功能,参数等关键信息
函数文档字符串有多种风格,比如说:Google 风格,NumPy/SciPy 风格和reStructuredText 风格
由于Pycharm会默认使用reStructuredText 风格,所以这里将使用这个风格来说明
接下来通过一个例子来说明:
1 | def pell_list(a, b, n): |
接下来来说明每个属性的内容:
:param + 变量名:用于说明变量的作用
:type + 变量名:用于说明变量的类型
:return + 变量名:用于说明返回的作用
:rtype:用于说明返回值的类型
需要注意的一点是,函数文档字符串可以通过help()访问,并且函数文档字符串会存储在__doc__里面
接下来展示另外两种风格:
首先是Google 风格
1 | def a_func(a): |
在Google 风格中,使用Args:标出变量的作用,使用Returns:标出返回值的作用,使用Raises:说明报错的触发条件
接下来是NumPy/SciPy 风格,这个风格的一大特点就是标准化,因为会使用到缩进和分割线来划分区域
1 | def a_func(a): |
可以看到,在这种风格中,Parameter用来说明变量对应的作用,同时在下一行用分割线划分区域,而Returns则用来说明返回的结果
类型注解
类型注解是Python中一种注明数据类型的方式,目的是为了方便理解对应数据的类型从而增强代码的可读性
使用的方法也很简单,只需要在对应的变量后面加上:即可,例如:
1 | def a_func(a : int) -> int: |
可以看到,这里变量a和b后面都注明了相应的类型
可以观察到,在函数后面有一个小箭头指着int,这是说明返回的类型是int
单元测试基础
接下来介绍单元测试基础,目的是测试代码能否正常工作,其核心为Python中的unittest库
在使用这个库的时候,我们会用到里面的这些东西:
TestCase 类:用于定义一个测试的方法,测试的主要步骤存放在这个类里面
测试方法:测试方法是一系列以test_开头的方法,相应的测试方式便存放在这里面
这里需要注意的是,测试方法的命名必须以test_开头,否则无法识别
这是因为unittest通过TestLoader类自动发现测试用例,其默认逻辑是:在继承TestCase的类中,只识别名称以test_开头的方法作为测试方法。
这样做的原因是为了使测试方法与一般的方法区分来开,如果全部都执行的话,可能会造成一些问题
断言方法:断言方法的作用是验证实际的结果与预计的结果是否一致(相当于if语句判断)
测试套件:测试套件的作用是让指定的测试文件批量执行,做到批量测试的效果,在大规模测试中可以一次性执行所有测试,不需要一次一次的执行测试文件
接下来举个例子来说明其使用方法
假设你有一个文件math_add.py,其代码如下:
1 | def add(a, b): |
这时候你想测试这个代码,于是你新建了一个测试文件math_add_test.py
首先需要导入unittest这个库
1 | import unittest |
同时又因为你需要测试的方法来自math_add.py里面,所以还需要导入这个文件的方法到这里面
1 | from math_add import add |
在完成以上的导入步骤后,接下来就是写这个测试文件的核心内容了
首先是继承unittest.TestCase方法,先写一个类,之后在这里类的类名后面写上要继承的方法即可:
1 | class TestMathAdd(unittest.TestCase): |
接下来是写类里面的测试方法:
由于这里是为了验证加法的结果是否正确,我们需要用到的断言方法是assertEqual(a, b)
这个断言方法的作用是判断a和b是否相等
这里由于add(要测试的方法)返回的结果就是两个参数之和,所以这里就直接填入即可
1 | class TestMathAdd(unittest.TestCase): |
这里还可以多加几次验证,最后的结果如下:
1 | class TestMathAdd(unittest.TestCase): |
那么要怎么运行测试呢?只需要在外部作用域加上unittest.main()即可开始测试
上文也提及到了,unittest.main()只会运行那些变量名为test_开头的方法
最后完整代码如下:
1 | import unittest |
运行后输出如下:
1 | Ran 1 test in 0.001s |
断言方法
在介绍完基础的测试例子后,接下来来讲讲断言方法,上文例子中使用的为self.assertEqual(a, b),此外还有很多
需要提示的一点是,下文方法中出现的msg对应的是验证不通过后的文本显示
| 断言方法 | 作用 |
|---|---|
| assertEqual(a, b, msg=None) | 用于验证a == b(a等于b) |
| assertNotEqual(a, b, msg=None) | 用于验证a != b(a不等于b) |
| assertTrue(x, msg=None) | 用于验证x是否为True |
| assertFalse(x, msg=None) | 用于验证x是否为False |
| assertIs(a, b, msg=None) | 用于验证a和b是否为用一个对象(这里不是单单指两者的值相等,例如a = 1,b = 1是不同的对象) |
| assertIsNot(a, b, msg=None) | 用于验证a和b是否为不同对象 |
| assertIsNone(x, msg=None) | 用于验证x是否为空值 |
| assertIsNotNone(x, msg=None) | 用于验证x是否不为空值 |
| assertGreater(a, b, msg=None) | 用于验证a是否大于b(a > b) |
| assertGreaterEqual(a, b, msg=None) | 用于验证a是否大于等于b(a >= b) |
| assertLess(a, b, msg=None) | 用于验证a是否小于b(a < b) |
| assertLessEqual(a, b, msg=None) | 用于验证a是否小于等于b(a <= b) |
| assertIn(a, b, msg=None) | 用于验证a是否为b的元素(a in b) |
| assertNotIn(a, b, msg=None) | 用于验证a是否不为b的元素(a not in b) |
| assertSetEqual(a, b, msg=None) | 用于验证集合a和集合b是否相等 |
| assertListEqual(a, b, msg=None) | 用于验证列表a与列表b是否相等(顺序相等) |
| assertDictEqual(a, b, msg=None) | 用于验证字典a和字典b是否相等 |
| assertMultiLineEqual(a, b, msg=None)/td> | 用于验证多行字符串是否相等,a和b都是多行的字符串 |
| assertRegex(s, regex, msg=None) | 用于验证字符串s是否匹配正则表达式regex |
| assertNotRegex(s, regex, msg=None) | 用于验证字符串s是否不匹配正则表达式regex |
| assertAlmostEqual(a, b, places=7, msg=None) | 用于验证a和b的差是否在10^(-places)范围内,这里places默认为7(需要这个的原因是判断浮点数的时候assertEqual可能不准,例如0.1 + 0.2 实际为0.30000000000000004,这里如果用assertEqual是不通过验证的) |
| assertRaises(exception, callable, *args, **kwargs) | 用于验证调用callable(*args, **kwargs)的时候是否会出现exception的异常(举个例子:self.assertRaises(ValueError, int, "abc"),这里会报错ValueError,所以通过验证) |
| assertRaisesRegex(exception, regex, callable, *args, **kwargs) | 用于验证抛出的错误是否匹配正则表达式regex |
| assertIsInstance(a, b, msg=None) | 用于验证a是否为类型b |
| assertNotIsInstance(a, b, msg=None) | 用于验证a是否不为类型b |
测试套件
测试套件是用来管理和组织多个测试用例(TestCase)的容器。可以组合测试用例,从而实现批量测试的结果
接下来开始一点一点说明如何操作
首先我们还是一样,选择要验证的文件
1 | def add(a, b): |
这里的文件名为math_add_mul.py
接下来还是照样,导入模块和继承方法
1 | import unittest |
这里由于需要演示测试套件,所以用了两个TestCase
完整的如下:
1 | import unittest |
接下来是创建一个测试套件,也就是unittest.TestSuite()
这个套件的作用是可以往里面添加测试,之后运行这个套件,可以实现一次性执行多个测试的效果
1 | def create_suite(): |
接下来是往这个套件里面添加测试,将会用到这个方法:addTest()
1 | suite.addTest(TestAdd('test_add')) |
在上面这个例子中,addTest里面填写的是类TestAdd,而类的里面填写的是要执行的测试
也就是这个:
1 | class TestAdd(unittest.TestCase): |
当然,这样一个一个添加还是太慢了,还可以用一个可迭代对象(比如列表)来添加
但需要注意的一点是这里用的不再是addTest(),而是addTests(注意末尾多了个s)
1 | suite.addTests([TestAdd('test_add'),TestAdd('test_add_negative')]) |
如果一个类里面有几百条测试,这样加还是有点慢,那有没有更快的方法呢?
当然有,你还可以把整个类添加进去,这里需要用到的还是addTest()方法,而在括号里面,需要用到另一个方法makeSuite()
这个方法的作用是测试指定类中的所有测试,同时把这些测试组装成一个TestSuite
1 | suite.addTest(unittest.makeSuite(TestMul)) |
在上面这个例子中,便是测试了类TestMul中的所有测试
相较于一个一个加测试,使用这种方法在大规模测试中会更加方便
完整的代码如下:
1 | def create_suite(): |
接下来是执行的部分
首先你需要创建一个实例用来测试:
1 | runner = unittest.TextTestRunner(verbosity=2) |
这里的verbosity = 2代表着详细输出,verbosity = 1代表了简略输出(用.表示通过,F表示失败)
此外还有stream参数,这个参数的作用是指定输出流,默认是sys.stdout(也就是控制台),可以自己重定向到文件中,这样就可以实现保存测试结果
另外一个参数为descriptions作用是是否显示测试方法中的文档字符串(Docstring),默认为True
在完成设置后,就得开始执行测试了,需要用到这个方法run()
1 | run_test.run(create_suite()) |
这里括号内的create_suite()便是之前定义的测试套件
最后完整代码如下:
1 | import unittest |
高级特性
偏函数
偏函数是functools模块中的一个功能,其主要作用是固定原函数的部分参数,而后生成一个新的函数
利用偏函数可以简化函数调用时的参数传递
首先需要导入这个模块:
1 | from functools import partial |
上面这个例子是导入了functools中的partial方法,也是偏函数的基础
接下来让我们从一个例子开始逐步讲解偏函数的使用:
首先我们这里假设个场景:要求对输入的数字进行加法运算
1 | # 需要运算的数字如下1,3,3 2,3,5 5,3,7 |
我们可以发现,在上面给的例子中,第二个数始终为3,这时候我们可以将3用偏函数固定,只输入a和c的值
1 | # 需要运算的数字如下1,3,3 2,3,5 5,3,7 |
接下来开始逐一解释
这里partial(a_func,b = 3)是偏函数的核心所在,第一个参数填的是函数名称,也就是要调用的函数,而第二个参数填的是要固定的参数名和对应的参数值
这里偏函数被赋值给了变量a_func_pa,在变量后面的是剩余未被固定的参数a和c
由于固定的参数是在中间的b,这里如果c不加关键字参数,那么函数会找不到参数。这里必须为关键字参数的原因是函数参数传递是按顺序的,而中间b函数无法直接跳过,所以只能用关键字参数传递
接下来再提供一个例子:二进制转为十六进制
1 | from functools import partial |
可以看到,这里将int()中的参数base固定为2,此时后面在输出十进制的时候便不需要再次说明base的值
缓存
缓存是Python中一个十分好用的装饰器,可以提高反复调用的函数的执行效率,主要使用到functools中的lru_cache
接下来通过一个例子来说明该如何使用
首先需要导入这个方法:
1 | from functools import lru_cache |
在导入后,我们需要把函数放到这个装饰器下:
1 |
|
这里装饰器括号内指的是缓存数量,超过这个数量就会删除旧的缓存同时生成新的缓存
之后便可以正常调用函数运行
接下来给出没有缓存和缓存的对比:
1 | import time |
可以看到,有缓存时,运行时间大大提高了
缓存存储
被lru_cache装饰的函数会新增两个方法cache_info()和cache_clear()
接下来分别来介绍如何使用
cache_info()
cache_info的作用是返回缓存的统计信息,其中包含四个部分:hits、misses、maxsize和currsize
hits:缓存命中次数(直接返回缓存结果的次数)misses:缓存未命中次数(首次计算并缓存的次数)maxsize:缓存最大容量currsize:当前缓存条目数
接下来给出例子:
以上面的例子作为示例
1 | import time |
这里函数wrapper()新增了一行wrapper.cache_info = func.cache_info
说明一下为什么要加这个
由于这里pell()套了两个装饰器,分别是@timer和@lru_cache
而这里真正返回变量的是外层@timer中的wrapper(),但是wrapper()只是一个普通的函数,没有cache_info
而加这一行的作用是把func(也就是pell())的cache_info传给wrapper(),这样的话wrapper()就有了cache_info
也就可以通过cache_info()调出缓存信息
而调出缓存的方法也很简单,具体为函数名.cache_info()
cache_clear
cache_clear的作用为清空缓存
使用方法同上:函数名.cache_clear:
需要注意的几个点
由于缓存本质上是用字典来储存的,所以函数的参数必须为可哈希
不可以用列表和字典
此外,如果函数的结果依赖于外部状态(如当前时间),缓存会导致返回过期结果。
说的直白一点就是数据不再更新