函数
函数
1,定义和调用函数
1,定义函数
在python中,使用def定义函数。语法格式如下:
1 | def 函数名([参数列表]): |
无参函数:当函数体内代码不需要外部传入参数时,也能够独立运行,那么就可以定义无参数的函数;
1 | def hi(): |
当函数没有参数时,也必须添加一对小括号,否则抛出语法异常
空函数:就是不执行任何操作的函数,在函数体内使用pass空语句填充函数体。如果缺少了pass,就会抛出语法错误。
1 | def no(): |
空函数的作用:可以作为占位符备用,在函数的具体功能还没有实现前,可以先定义空函数,让代码能运行起来,事后再编写函数体代码
有参函数:当函数体内代码必须依赖外部传入参数时,那么就可以定义有参数的函数
1 | def abs(x,y): |
函数的参数放在函数名后面的小括号内,可以设置一个或多个参数,以逗号分隔。函数的返回值通过return语句设置。
在函数体内,一旦执行return语句,函数将返回结果,并立即停止函数的运行。如果没有return语句,执行完函数体内所有代码后,也会返回特殊值(None)。如果明确让函数返回None。可以按如下方式编写。
1 | return None |
【拓展】
再定义函数时,如果在函数体第一行使用”””或””添加多行注释时,则在调用函数时,会自动提示注释信息。
1 | def abs(x): |
因此,可以在函数体第一行添加函数的帮助信息,如函数的功能、参数类型和作用、返回值类型等信息
2,调用函数
定义函数后,函数内的代码不会自动执行,只用调用函数时,函数体的代码才被执行。使用小括号可以直接调用一个函数。语法格式如下:
1 | 函数名([参数列表]) |
如果在定义函数时没有设置参数,则调用函数时不用传入参数。
如果在定义函数时设置了多个参数,则调用函数时必须传入同等数量的参数,否则抛出TypeError错误。如果传入的参数数量是对的,但参数类型不能被函数所接收,也会抛出TypeError错误,并且给出错误信息。
1 | hi() |
调用函数的3种形式:
1 | 1,语句: |
函数都是先定义,后调用。在定义阶段发现语法错误,将会提示错误,但是不会判断逻辑错误,只有在调用函数时,才会判断逻辑错误。
3,定义嵌套函数
python允许创建多级嵌套的函数,就允许在函数内部定义函数,这些内部函数都是遵循各自的作用域和生命周期等规则。
嵌套函数:内层函数可以访问外层函数作用域中的变量,但是外层函数不允许访问内层函数作用域中的变量。
1 | def outer(): |
2,函数的参数和返回值
1,形参与实参
函数的参数分为两种:
形参:在定义函数时声明的参数变量,尽在函数内部可见
实参:在调用函数时实际传入的值。
1 | def r(a,b): |
上面的示例中。a,b为形参,x,y为实参。在执行函数时,Python把实参变量的值赋值给形参变量,实现参数的传递。
根据实参的类型不同,可以把实参分为两种类型:
固定值:实参为不可变对象,如数字,布尔值,字符串,元组
可变值:实参为可变对象,如列表,字典和集合
1 | def fun(obj): |
2,位置参数
位置参数就是根据位置关系有序把实参的值有序传递给形参。在一般情况下,实参和形参应该是一一对应的,不能够错位传递,否则会引发异常或者运行错误。
【提示】
1 | 在调用函数时,实参和形参必须保持一致,具体说明如下: |
在python中,内置函数会自动检查传入值的个数和类型,如果个数或类型不一致,则会引发异常。例如,调用内置函数abs(),并传入字符串,则抛出TypeError错误。
对于自定义函数,Python会自动检查实参个数,如果实参和形参个数不一致,将抛出TypeError错误。但是Python不会检查传入值的类型和形参类型是否一致。
如果实参于形参的位置顺序不对应,虽然Python不会自动检查,但是容易引发异常或逻辑错误
3,关键字参数
在调用函数时,实参一般是按顺序传递给形参的。
1 | def test(a,b,c): |
实参1,2,3按顺序传入函数,函数能够按顺序把他们分配给形参变量a,b,c。
关键字参数能够打破参数的位置关系,根据关键字映射实现给形参赋值。例如:
1 | def test(a,b,c): |
关键字参数是针对调用函数时传递的实参而言,而位置参数是针对定义函数时设置的形参而言。
位置参数和关键字参数可以混用,一般位置参数在前,关键字参数在后。
一旦使用关键字参数后,其后不能使用位置参数。因为这样会重复为一个形参赋值,应确保形参和实参个数相同。
例如:test(c = 3,b=2,1)
4,默认参数
调用Python内置函数int(),可以传递一个参数,也可以传递两个参数。
int()函数的第2个参数用来设置转换进制,默认是十进制(base=10)。因此,默认参数的作用是能够简化函数调用,当需要的时侯,也允许传入额外的参数来覆盖默认参数值。
1 | def 函数值(参数1,参数2,...,参数n=默认值n,参数n+1=默认值n+1,...): |
当定义函数时,如果某个参数的值不经常变动,就可以考虑将这个参数设置为默认参数。使用默认参数之后,位置参数必须放到前面,默认参数放到后面。位置参数和默认参数没有个数限制。
1 | def power(x,n): |
默认参数是在定义函数时赋值的,且仅赋值一次。当调用函数时,如果没有传入实参值,函数就会使用默认值。
应避免使用可变参数作为参数的默认值
1 | def sum(num,scores=[]): |
预设程序运行的结果是[12]、[24],但是实际结果是[12]、[12,24]。出现问题的原因在于scores的默认值是一个列表对象,而列表对象,而列表对象是一个可变类型,那么使用append()方法添加列表元素时,不会为score重新创建一个新的列表,而是在原来的对象的基础上只想操作。
因此,对于默认参数,如果默认值是不可变类型,那么多次调用函数是不会相互干扰的,如果默认值是可变参数,那么在调用函数时就要重新初始化可变参数,避免多次调用的相互干扰。
5,可变参数
可变参数就是允许定义能和多个实参相匹配的形参。当无法确定实参个数时,使用可变参数是最佳选择。定义可变参数有两种形式,具体说明如下。
1,单星号形参
当定义参数时,在形参名称前添加一个星号前缀,就可以定义一个可变的位置参数,其语法格式如下:
1 | def 函数名(*param): |
声明一个类似*param的可变参数时,从此处开始直到结束的所有位置参数都将被收集,并汇集成一个名为param的元组中。在函数体内可以使用for语句循环遍历param对象,读取每个元素,实现对可变位置参数的读取。
示例一:定义一个求和函数,能够把参数中所有数字进行相加并返回。
1 | def num(*sums): |
使用可变参数进行参数传递,设计求和函数会显得非常的方便。
python也允许传入单星号实参。在调用函数时,当在实参前添加星号(*)前缀,python会自动遍历该参数对像,提取所有元素,并按顺序转换为位置参数。因此,要确保被添加星号的实参是可迭代对像。这个过程也被称为解包位置参数。
示例二:调用函数sum(),并传入可遍历数据对像
1 | def num(*nums): |
2,双星号形参
当定义函数时,在形参名称前面添加两个星号前缀,就可以定义一个可变的关键字参数,其语法格式如下:
1 | def 函数名(**param): |
声明一个类似*param的可变参数时,从此处开始直到结束的所有位置参数都将被收集,并汇集成一个名为param的字典中。在函数体内可以使用for语句循环遍历param对象,读取每个元素,实现对可变位置参数的读取。
示例三:定义一个求和函数,能够接受关键字传递的参数,并把所有键,值进行汇总,如果值为数字,则叠加并记录,最后反回一个元组,包含可汇总的键的列表,以及汇总值的和。
1 | def sum(**nums): |
python也允许传入双星号实参。在调用函数时,当在实参前添加双星号(**)前缀,python会自动遍历该参数对像,提取所有元素,并按顺序转换为关键字参数。因此,要确保被添加双星号的实参为字典对像。这个过程也被称为解包关键字参数。
示例四:针对示例三,先定义一个字典对象,然后再调用函数sum(),并传入字典对象作为可变参数,则也可以得到相同的结果。
1 | def sum(**nums): |
示例五:利用可变参数可以定义创建字典对象的函数
1 | def dict(**kwargs): |
6,混合使用参数
位置参数、默认参数,关键字参数和可变参数可以混合使用。混用时的位置顺序如下:
在定义函数时,形参位置顺序:位置参数在前,默认参数在后
1 | (位置参数,默认参数,可变位置参数,可变关键字参数) #默认参数会被重置 |
在调用函数时,实参位置顺序:位置参数在前,关键字参数在后。
1 | #推荐顺序 |
示例一:定义一个函数,包含位置参数和默认参数,在调用函数时,使用位置参数和关键字参数。
1 | def f(name,age,sex=1): |
示例二:可变位置参数和可变关键字参数混用:可变参数在前,可变关键字参数在后。
1 | def f(*args,**kwargs): |
示例三:可变位置参数与位置参数和默认参数混合使用
1 | #可变位置参数放在位置参数的后面,默认参数放在所有参数的最后 |
1 | #可变位置参数放在所有的参数的最后,默认参数放在位置参数的后面 |
示例4:可变关键字参数与位置参数和默认参数混合使用。
1 | #默认参数要放在位置参数的后面,可变关键字参数放在最后。 |
【注意】默认参数不能放在可变关键字参数的后面,否则将抛弃语法错误。
示例五:位置参数,默认参数,可变位置参数和可变关键字参数混用
1 | #如果要保持使用默认参数的默认值时,默认参数的位置应该位于可变位置参数之后。 |
7,函数的返回值
在python函数体内,使用return语句可以设置函数的放回值,语法格式如下:
1 | return [表达式] |
【注意】如果一个函数没有return语句,其是他有一个隐含的return语句,返回值是False,类型是NoneType。与return或则return none等价,都是返回none。
示例一:以下三个返回值返回none。
1 | def f1(): |
retun语句还结束函数调用的功能。
示例2:把return语句放在函数体内第二行,这将导致return语句后免得语句无法被执行,因此仅看到1语句被执行,而3语句无法被执行。
1 | def f(): |
在函数体内可以设置多条return语句,但是只有一条语句被执行。如果没有任一一条被执行,那么就会隐式调用的return None语句作为返回值。
示例三:在排序函数中,经常会用到以下设置,通过比较两个函数的大小,确定他们排序顺序。
1 | def f(x,y): |
函数的返回值可以是任意类型,但是返回值只能是单值,值可以是包含多个元素的对象,如列表,字典等,因此要返回多个值,可以考虑把多个值放到列表中,元组,字典等对象中再放回。
示例4:在本示例中,虽然return语句后面跟随多个值,但是python会把他们隐式封装起来成为一个元组对象返回。
1 | def f(): |
3,匿名函数
1,定义匿名函数
在python中,使用lamda运算符可以定义匿名函数,语法格式如下:
1 | fn = lamda [arg1 [arg2,...,argn]]:expression |
具体说明:
[arg1 [arg2,…,argn]]:可选参数,表示匿名函数的参数,参数的个数不限,参数之间通过逗号分隔;
expression:表示必选参数,为一个表达式定义函数体,并能够访问冒号左侧的参数
fn:表示一个变量,用来接收lamda表达式的返回值,返回值为一个匿名函数对象,通过fn变量可以调用lamda函数。
lamda是一个表达式,而不是一个语句块,它具有以下的几个特点:
1 | #与def语句的语法相比较,lamda不需要小括号,冒号(:)左侧的值表示函数体的参数,函数不需要return语句,冒号右侧表达式的运算结果就是返回值。 |
lamda表达式也会产生一个新的局部作用域,拥有独立的命名空间。在def定义的函数中嵌套lamda表达式,lamda表达式可以访问外层的def定义的函数中可用的变量。
示例一:定义一个无参匿名函数,直接返回一个固定值
1 | t = lamda :True |
等价于def func():return True
示例二:定义一个带参数的匿名函数,用来求两个数字之和。
1 | #求和匿名函数 |
其中,Sum=lambda a,b:a+b就等效于def sum(a,b):return a+b
示例三:通过一行代码把字符串中的各种空字符转换为空格
1 | print((lambda s:' '.join(s.split()))("this is\na\ttest")) |
输出为:
1 | this is a test |
上面一行代码等价于:
1 | s = "this is\na\ttest" |
示例四:在匿名函数中设置默认值
1 | c= lambda x,y=2:x+y |
示例五:
快速转换为字典对象
1 | c = lambda **arg:arg |
示例六:通过匿名函数设置字典排序的主键
1 | infors = [ |
示例七:通过匿名函数设置一个高阶函数
1 | def test(a,b,func): |
示例八:通过匿名函数过滤出能够被3整除的元素
1 | d=[1,2,4,67,85,34,45,100,456,34] |
【提示】
过滤器函数fliter(function,iterable)包含两个参数,参数function是筛选函数,参数iterable是可迭代对象。filter()可以从序列中过滤出符合条件的元素,d=filter(lambda x:x%3==0,d)的含义就是从序列中筛选出符合函数lambda x:x%3==0的新序列。
2,序列处理函数和lambda表达式
使用序列处理函数来操作序列对象,包括filter(),map(),reduce()和sorted()。
1 | filter(function,iterable):根据函数过滤函数 |
在python2中reduce()是内置函数,而在python3中归为functools模块,因此需要导入该模块。
案例完整代码如下所示,演示效果如下:
1 | from functools import reduce |
1 | 运算结果: |
4,变量作用域
变量作用域是指变量(scop)是指变量在程序中可以被访问的有效范围,也成为命名空间、变量的可见性。python的作用域是静态的,在源代码中定义变量的位置决定了该变量能够被访问的范围,即python变量的作用域由变量所在源代码中的位置决定。
1,定义作用域
在python中,并不是所有的语句块中都会产生作用域。只有模块、类、函数才会产生作用域。
【示例】分别使用class和def创建作用域,并演示访问作用域内变量的方法
1 | class C(): |
在作用域中定义的变量,一般只能在作用域内可见,不允许在作用域外直接访问
【提示】
在条件,循环,异常处理,上下文管理器等语句块中不会创建作用域。
2,作用于类型
在python中,作用域可以分为四种类型,简单的说就是
1,L(local)级:局部作用域
每当函数被调用时都会创建一个新的局部作用域,包括def函数和lambda表达式函数。如果是递归的函数,每次被调用都会创建一个新的局部变量域。在函数体内,除非使用global关键字声明变量的作用域为全局作用域,否则默认都为局部变量。局部作用于不会持续存在,存在的时间依赖与函数的生命周期。所以,一般建议都尽量避免定义全局变量,因为全局变量在模块运行的过程中会一直存在,占据着内存。
2,E(enclosing)级:嵌套作用域
嵌套作用域也是函数作用域,与局部作用域是相对关系。相对于上一层的函数而言,嵌套作用域也是局部作用域。对于一个函数而言,L表示定义在该函数内部的局部作用域,而E表示是定义在此函数的上一层父级函数中的局部作用域。
3,G(global)级:全局变量
每一个模块都是一个全局作用域,在模块中声明的变量都具有全局作用域。从外部来看,全局变量就是一个模块对象中的属性。
【注意】全局作用域的作用范围仅限于单个模块文件内
4,B(bulit-in)级:内置作用域
在系统内置模块里定义的变量,如预定义在bulit-in模块内的变量。
3,LEGB解析规则
变量名的LEGB解析规则:当在函数中使用未确定的变量名时,Python会按照优先级依次搜索4个作用域,以此来确定该变量名的意义。
1 | 局部作用域>嵌套作用域>全局作用域>内置作用域 |
具体解析步骤如下:
第1步,在局部作用域(L)中搜索变量。
第2步,如果在局部作用域中没有找到变量,则跳转到上一层嵌套结构中,访问def或lambda函数的嵌套作用域(E)。
第3步,如果在函数作用域中没有找到同名变量,则向上访问全局作用域(G)
第4步,如果在全局作用域中也没有找到同名变量,最后访问内置函数作用域(B)
根据上述顺序,在第一处找到的位置停止搜索,并读取变量的值。如果在整个作用域链上都没有找到,则会抛出NameError异常。
示例一:比较全局作用域和局部作用域的优先级关系。
1 | n = 1 |
输出为
1 | 2 |
在上面的代码中,有一个全局变量n,值为1,在函数f()中定义布局变量n,值为2,在函数内部输出n变量时,会优先搜索局部作用域,所以打印输出2,在全局作用域打印n为1。
示例二:在示例1的基础上再添加一个嵌套作用域,然后比较局部作用域、嵌套作用域和全局作用域之间的优先级关系。
1 | n=1 |
对于sub()函数来说,当前局部作用域中没有变量n,所以在L层找不到,然后在E层搜索,当在嵌套函数f()中找到变量后,就直接读取并打印输出,不再进一步搜索G层全局作用域。
示例三:在示例一基础上调整print(n)和n=2两条语句的顺序。
1 | n=1 |
输出为:
1 | 发生异常: UnboundLocalError |
在Python预编译期,局部变量n被定义,但没有赋值。当执行程序时,不会到全局作用域去搜索变量n。当使用print()打印变量n时,局部变量n并没有绑定对象,既没有被赋值,抛出变量在分配前被引用的错误。所以,在引用一个变量之前,一定要先赋值。
为什么本例会触发UnboundLocalError异常,而不是NameError:name ‘n’ is not dedfind。Python模块代码在执行之前,并不会经过预编译,但是函数体代码在运行前会经过预编译,因此不管变量名绑定在哪个函数作用域上,都能被编译器知道。Python虽然是一个静态作用域语言,当变量访问是动态的,直到在程序运行时,才会发现变量引用问题。
示例4,在示例一基础上删除局部变量n=2
1 | n=1 |
输出为:
1 | 1 |
在上面的示例中,先访问局部作用域,没有找到变量n,所以在打印时直接找到了全局变量n,然后的读取并输出。
4,跨越修改变量
一个非L层的变量相对于L层变量而言,默认是只读,而不能修改。如果希望在L层中修改定义在非L层的变量,可以为其绑定一个新的值,Python会认为是在当前的L中引入了一个新的变量,即便内外两个变量重名,却也有着不同的意义,而且在L层中修改新变量不会影响到非L层的变量。如果希望在L层中修改非L层中的变量,则可以使用global、nonlocal关键字。
1,global关键字
示例一:如果希望在L层中修改G层中的变量,可以使用global关键字。
1 | n = 1 |
在上面代码中,使用global关键字之后,在sub()函数中使用的n变量就是全局作用域中的n变量,而不会新生成一个局部作用域中的n变量。
2,nonlocal关键字
使用nonlocal关键字可以实现在L层中修改E层中的变量。这时Python3新增的特性。
示例二:针对示例一,修改其中代码,把全局变量移到嵌套作用域中,然后使用nonlocal n命令在本地作用域中修改嵌套作用域中变量n的值。
1 | def f(): |
在上面的代码中,由于声明了nonlocal,这样在sub()函数中使用的n变量就是E层(即f()函数中)声明的n变量,所以输出两个2。
5,修改嵌套作用域变量
除了使用nonlocal关键字外,也可以使用列表等可变类型的容器间接修改嵌套作用域变量。本例比较演示这两种嵌套作用域变量的修改方法。
1 | #使用列表等可变容器包含待修改的值 |
6,比较全局变量和局部变量
使用下面两个Python内置函数可以访问全局变量和局部变量。
globals():以字典类型返回当前位置的全部全局变量。
locals():以字典类型返回当前位置的全部局部变量。
【注意】
1 | 通过globals()和locals()返回的字典对象可以读,写全局变量和局部变量。 |
下面案例简单比较了全局变量和局部变量的不同用法。
1 | d = 4 |
运行结果:
1 | 全局变量 d: 4 |
5,闭包和装饰器
Python支持函数闭包和装饰器的特性,它们都是特殊结构的嵌套函数,具有特殊功能和用途。
1,定义闭包
闭包就是一个在函数调用时所产生的,持续存在的上下文活动对象。
1,形成原理
函数被调用时,会产生一个临时的上下文活动对象(namespace),他是函数作用域的顶级对象,作用域内所有局部变量,参数,内层函数等都将作为上下文活动对象的属性而存在。
在默认情况下,当调用函数后,上下文活动对象会被立即释放,避免占用系统资源,但是,当函数内的局部变量,参数,内层函数被外界引用时,则这个上下文活动对象会继续存在,直到所有外部引用全部被注销。
但是,函数作用域是封闭的,外界无法访问,那么如何外界才能访问到函数内的私有成员呢?
根据作用域链,内层函数可以访问外层函数的私有成员。如果内层函数引用了外层函数的私有成员,同时内层函数又被传送给外部变量,那么闭包就形成了,这个函数就是一个闭包体,当它被调用时后,这个函数的上下文活动对象就暂时不被注销,其上下文环境会一直存在,通过内层函数,可以不断读,写外层函数的私有成员。
2,闭包结构
典型的闭包体就是一个嵌套结构的函数。内层函数引用外层函数的私有成员,同时内层函数又被外界引用,当外层函数被调用后,就形成了闭包,这个函数也称为闭包函数。
下面是一个典型的闭包结构。
1 | def outer(x): |
【示例一】在嵌套结构中的函数中,外层函数使用return语句返回内层函数,在内层函数中包含了对外层函数作用域中变量的引用,一旦形成闭包体,就可以利用返回的内层函数的closure内置属性访问外层函数闭包体。
1 | def outer(x): |
输出为:
1 | (<cell at 0x00000145A6777E80: int object at 0x00000145A3BA00F0>,) |
【示例二】使用闭包实现优雅的打包,定义临时寄存器
1 | def f(): |
在上面的示例中,通过外层函数设计一个闭包体,定义一个持久的寄存器。当调用外层函数生成上下文对象之后,就可以利用返回的内层函数不断地向闭包体内的局部变量a递加值,该值会一直存在。
2,函数装饰器
装饰器是Py的一个重要特性,本质上他就是一个以函数作为参数,并放回一个函数的函数。装饰器可以作为一个函数在不需要修改代码的前提情况下增加其他功能,常用于有切面请求的场景,通常是,插入日志,增加计时逻辑,性能测试,事务处理,缓存,权限校验等场景。有了装饰器,开发人员就可以抽离出大量与函数功能无关的雷同的代码并不断重用。
装饰器的语法以”@”开头,接着是装饰器函数的名称和可选的参数,然后是被装饰的函数,具体语法:
1 |
|
其中decorator表示装饰器,dec_opt_args表示装饰器可选的参数类型,func_decorator表示装饰器函数名称,func_decorator_args表示装饰的函数的参数。
结合实例介绍装饰器的用法:
【示例一】:定义一个简单的函数
1 | def foo(): |
为增加函数的新功能:记录函数的执行日志
1 | def foo(): |
为了减少重复代码,可以定义一个函数:专门处理日志,处理完日志后再执行业务代码。
1 | def logging(func): |
P使用’@’作为装饰器的语法操作符,方便应用装饰函数。针对示例一,下面使用”@”语法应用一下装饰器函数。
1 | def logging(func): |
装饰器相当于执行了装饰器logging函数后,又返回被装饰函数foo,因此foo()被调用时相当于执行了两个函数,等价于logging(foo())
3,案例,为装饰器设置参数
1,对带参数的函数进行装饰
【示例一】:设计业务函数需要传入的两个参数并计算值,因此需要对装饰器函数内部的嵌套函数进行改动。
1 | def logging(func): |
2,解决函数参数数量不确定的问题
【示例二】示例一展示了参数个数固定的应用场景,不过可以使用Python的可变参数args和*kwargs来解决参数数量不确定的问题。
1 | def logging(func): |
运行结果:
1 | bar is running |
3,装饰器带参数
【示例三】在某些情况下,装饰器可能也需要参数,这时就需要使用高价函数来进行设计。针对实例2,为logging装饰器再嵌套一层函数,然后在外层函数中定义一个标志参数lock,默认参数值为True,表示打开日志打印功能:如果为False,则关闭日志功能。
1 | def logging(lock = True): |
运行结果:
1 | bar is running |
4,案例:解决装饰器的副作用
使用装饰器可以简化代码的编写,但是它也有一个缺点:被装饰的原函数的原信息被覆盖了,如函数的doc(文档字符串)、name(函数名称)、code.co.varnames(参数列表)等。
例如针对上一节示例3,输入如下代码。
1 | print(bar.__name__) |
将打印sub(),而不是bar,这种情况在使用反射函数时间就会带来问题,不过使用functools.wraps()函数可以解决这个问题。
【示例】导入functools模块,然后使用functools.wrap()函数恢复参数函数的元信息,这样当调用装饰器后,被装饰的函数的元信息重新被恢复为原来的状态。
1 | import functools |
5,案例:设计函数调用日志装饰器
下面的案例将完善装饰器的功能,使其能够适应带参数和不带参数等不同的应用需求。在装饰函数时,可以按默认设置把日志信息写入当前目录下的out.log文件中,也可以指定一个文件,日志信息主要包含函数调用的时间
1 | from functools import wraps #调用functools模块中的wrap函数 |
运行结果:
1 | bar was called 2023-10-27 13:06:08 |
6,案例:使用lambda表达式定义闭包
使用lambda表达式定义的匿名函数与def函数一样,也拥有独立的作用域。当在嵌套结构中的闭包体中使用lambda表达式替代def函数,有时会使代码更加简洁、易读。
1 | # 1.不使用lambda表达式 |
运行结果:
1 | 64 |
7,案例:解决闭包的副作用
闭包的优点:
1 | #实现在外部访问函数内的变量。函数是独立的作用域,它可以访问外部变量,但外部无法访问内部变量。 |
闭包的缺点:
1 | #常驻内存会增大内存负担。无节制的滥用闭包,容易造成内存泄露。 |
下面结合示例介绍如何解决闭包的第3个缺点。
【示例一】下面示例分别使用lambda表达式和嵌套结构 的函数定义闭包,返回两个闭包函数,在闭包函数内引用了外层函数的变量i,计算参数的i次方。
1 | #使用lambda表达式 |
1 | # 使用嵌套结构函数 |
由于延迟绑定的缘故,当调用闭包函数时,两个函数所搜索的外层变量i的值此时都为2,打印结果如下
1 | 4 9 |
【示例二】针对示例1存在的问题,可以使用形参的默认值立即绑定变量
1 | #使用lambda变量 |
1 | #使用嵌套结构函数 |
运行结果:
1 | 2 9 |
在生成内层函数时,把外层变量i的值赋值给内层函数的形参i,立即绑定变量,这样在内层函数中
访问时,就是形参变量i,而不是外层函数的变量i。此时两个闭包函数引用i的值就与循环过程中i的值保持一致,分别为1和2,打印结果如下:
1 | 2 9 |
6,递归函数
递归就是调用自身的一种方法,是循环计算的一种算法模式,在程序中广泛应用。
1,定义递归函数
递归函数就是一个函数直接或间接地调用自身。一般来说,递归需要有边界条件、递归前进段和递归返回段、当边界条件不满足时,递归前进;当边界条件满足时,递归就返回。在递归调用的过程中,系统会每一层的调用进行缓存,因此递归运算会占用大量的系统资源,过多的递归次数容易导致内存溢出。
递归必须由以下两部分组成。
1 | #递归调用的过程 |
在没有限制的情况下,递归运算会无终止地调用自身。因此,在递归运算中要结合if语句进行控制,只有在某个条件成立时才允许执行递归,否则不允许调用自身。
递归运算的应用场景如下:
1,求解递归问题
主要解决一些数学运算,如阶乘函数,幂函数和斐波那契数列。
【示例1】使用递归运算来设计阶乘函数
1 | def f(x): |
在这个过程中,利用if语句把递归结束的条件和递归运算分开。
2,解析递归型数据结构
很多数据结构都具有递归性,如DOM文档树、多级目录结构、多级导航菜单、家族谱系结构等。
对于这类数据结构,使用递归算法进行遍历比较合适
【示例2】Python递归遍历目录下所有文件。
1 | #遍历filepath下所有文件,包括子目录 |
3,适合使用递归法解决问题
有些问题最适合采用递归的方法求解,如汉诺塔问题。
【示例3】使用递归运算设计汉诺塔演示函数。参数说明:n表示金片数;a,b,c表示柱子,注意排序顺序。返回说明:当指定金片数和柱子名称时,将输出整个移动的过程。
1 | def f(n,a,b,c): |
运行结果:
1 | 移动【盘子1】 从 【A柱】 到 【C柱】 |
2,尾递归
尾递归是递归的一种优化算法,他从最后开始计算,每递归一次就算出相应的结果,并把当前的运算结果(或路径)放在参数里传给下一层函数。在递归的尾部,立即返回最后的结果,不需要递归返回,因此不用缓存中间调用对象。
【示例】下面是阶乘的一种普通线性递归运算
1 | def f(n): |
使用尾递归算法后,则可以使用如下的方法。
1 | def f(n,a): |
当n=5时,线性递归的递归过程如下:
1 | f(5) = {5 * f(4)} |
而尾递归的递归过程如下:
1 | f(5) = f(5,1) |
很容易看出,普通递归比尾递归更加消耗资源,每次重复的过程调用都使得调用链条不断加长,使系统不得不使用栈进行数据保存与恢复,而尾递归就不存在这样的问题,因为他的状态完全由变量n和a保存。
【提示】
1 | 从理论上分析,尾递归也是递归的一种类型,不过他的算法具有迭代算法的特征。上面的阶乘尾递归可以改写为下面的迭代循环。 |
3,递归与迭代
递归和迭代都是循环运算的一种方法。简单比较如下。
1 | 在程序结构上,递归是重复调用函数自身实现循环,迭代是通过循环语句实现循环。 |
【注意】
在实际应用中,能不用递归就不用递归,递归都可以用迭代来代替
【示例】下面拿斐波那契数列为例进行说明。
斐波那契数列就是一组数字,从第3项开始,每一项都等于前两项之和。例如:
1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181
使用递归函数计算斐波那契数列,其中最前面的两个数字是0和1。
1 | def fibonacii (n): |
尝试传入更大的数字,会发现递归运算的次数加倍递增,速度加倍递减,返回值加倍放大。如果尝试计算100的斐波那契数列,则需要等待很长的时间。
下面使用迭代算法来设计斐波那契数列,代码如下,基本没有任何延迟。
1 | def fibonacii(n): |
下面使用高阶函数来进行设计,把斐波那契数列函数封装在一个闭包体内,然后返回斐波那契数列函数,在闭包内使用memo字典持久记录每级斐波那契数列函数的求值结果,在下一次求值之前,先在字典中检索是否存在同级(数列的个数,字典的键)计算结果,如果存在,则直接返回,避免重复行计算;如果没有找到结果,则调用斐波那契数列函数进行求和。实现代码如下:
1 | def fibonacii(): |
运行结果:
1 | 354224848179261915075 |
4,案例:猴子吃桃
有一只猴子,第1天摘下若干个桃子,当即吃了一半,不接馋,又多吃了一个。第2天早上又将剩下的桃子吃掉了一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少个桃子。
【设计思路】
第10天时,还剩下一个桃子,所以peach(10)=1.
第1-9天时,peach(day - 1) =peach(day)/2-1,由此可见,peach(day) = (peach(day+1)+1)*2,采用递归思路即可求解。
【实现代码】
1 | def peach(day:int) -> int: |
【提示】
1 | Python从3.5版本开始,引入了类型注解 ,用于类型检查,防止运行时出现参数和返回值类型、变量类型不符合。 |
5,案例:角谷定理
角谷定理:任意输入一个大于1的自然数,如果为偶数,则将它除以2;如果为奇数,则将它乘以3加1.经过如此有限次计算后,总可以得到自然数值1,求经过多少此可以得到自然数1.
1 | num = int(input("请输入一个整数:")) |
运行结果:
1 | 请输入一个整数:7 |
7,案例实战
1,函数合成
高阶函数是函数式编程最显示的特征,起形式应至少满足下列条件之一。
1 | #函数可以作为参数被传入(即回调函数),如函数合成运算 |
compose(函数合成)和(柯里化)是函数式编程两种最基本的运算,他们都利用了函数闭包的特性和思路来进行设计。
【问题提出】
在函数式编程中,经常见到如下表达式运算。
1 | a(b(c(x))); |
这是“包菜式”多层函数调用,不是很优雅。为了解决函数多层调用的嵌套问题,需要用到函数合成。合成语法如下:
1 | f = compose(a,b,c) |
例如:
1 | def compose(f,g): |
上面的代码中,compose()函数的作用就是组合函数,将函数串联起来执行。将多个函数组合起来,函数的输出结果是另一个函数的输入参数,一旦第1个函数开始执行,就会像多米诺骨牌一样开始执行了。
【注意】
1 | 使用compose()函数需要注意以下三点。 |
【实现代码】
下面来完善compose()实现,实现无线函数合成
1 | #函数合成,从右到左合成函数 |
在上面的代码中,compose()实现从右到左进行合成,也提供了从左到右的合成,即composeLeft(),同时在compose()内添加了一层函数的校验,允许传递一个或多个参数。
【应用代码】
1 | add = lambda x : x + 5 |
最后3种组合方式都返回30。注意,排序顺序要保持一致
2,函数柯里化
【问题提出】
函数合成是把多个单一参数的函数合成为一个多参数的函数运算。例如,a(x)和b(x)组合为a(b(x)),则组合为f(a,b,x)。
【注意】
1 | 这里的a(x)和b(x)都只能接收一个参数。如果接收多个参数,如a(x,y)和b(a,b,c),那么函数合成就比较麻烦。 |
与函数合成相反的运算就是函数柯里化。所谓柯里化,就是把一个多参数的函数转化为一个单一参数的函数。有了柯里化运算之后,就能让所有函数只接收一个参数,实现分布运算。
【设计思路】
定义一个闭包体,先记录函数部分所需要的参数,然后异步接收剩余参数。也就是说,把多参数的函数分解为多步运算的函数,以实现每次调用函数时,仅需要传递更少或单个参数。例如,下面是一个简单的求和函数add()。
1 | def add(x,y): |
每次调用add(),需要同时传入2个参数,如果希望每次仅传入1个参数,可以这样实现柯里化:
1 | def add(x): |
函数add()接收一个参数,并返回一个参数,这个返回的参数可以再接收一个参数,最后返回两个参数数之和。从某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的函数式运算方法。
【代码实现】
设计curry可以接收一个函数,即原始函数,返回的也是一个函数,及柯里化函数。这个返回的柯里化函数在执行过程中会不断的返回一个存储了传入参数的函数,直到触发了原始函数执行的条件。例如,设计一个add()函数,计算连个参数之和。
1 | def add(x,y): |
柯里化函数
1 | curryAdd = curry(add) |
这个add()需要两个参数,但是执行curryAdd()时可以传入一个参数,当传入的参数少于add()需要的参数时,add()函数并不会执行,curryAdd()就会将这个参数记录下来,并且返回林外一个函数,这个函数可以继续接收传入参数。如果传入参数的总数等于add()需要参数的总数,就执行原始参数,返回想要的结果。或则没有参数限制,最后根据空的小括号调用作为执行原始参数的条件,返回运算结果。
【封装代码】
1 | #函数柯里化 |
【应用代码】
应用函数无形参限制
【示例一】设计求和函数没有形参限制,函数柯里化将根据空小括号作为最后调用原始函数的条件。
1 | %求和函数,参数不限 |
应用函数有形参限制
【限制2】设计求和函数,返回3个参数之和。
1 | def add(a,b,c): |
curry函数的设计不是固定的,可以根据具体应用场景灵活定制。curry主要有3个作用:缓存参数、暂缓函数执行、分解执行任务。