注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

Python函数式编程:函数详解

Python编程 彭东稳 3211次浏览 已收录 0个评论

一、Python函数

函数是Python为了代码最大程度地重用和最小化代码冗余而提供的基本程序结构。函数是一种设计工具,它能让程序员将复杂的系统分解为可管理的部件。函数用于将相关功能打包并参数化。需要注意的是函数只能返回一个值,如果return语句后面有多个逗号分隔的值,会自动的封包成一个元祖。另外一个函数可以有任意多个return语句,但是始终只会执行一个return语句,执行return语句,会返回到调用方的作用域。

Python提供了很多内置函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数。

再谈作用域,在Python中作用域分为全局作用域和函数作用域。函数定义了函数作用域,应用范围仅存在与一个函数体内;而模块定义了全局作用域,每个模块都是一个全局作用域,因此全局作用域的范围仅限于单个程序文件。可以使用locals()函数查看当前作用域内的相关变量。

二、函数设计规范

2.1 耦合性(降低)

  • 通过参数接受输入,以及通过return产生输出以保证函数的独立性。
  • 尽量减少使用全局变量进行函数通信。
  • 不要在函数中修改可变类型的参数。
  • 避免直接修改定义在另外一个模块中的变量。

2.2 聚合性(提高)

  • 每个函数都应该有一个单一的、统一的目标。
  • 每个函数的功能都应该相对简单。

三、调用函数

Python内置了很多有用的函数,我们可以直接调用。要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs,只有一个参数。

调用函数的时候,如果传入的参数数量不对,会报TypeError的错误,并且Python会明确地告诉你有几个参数

如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报TypeError的错误,并且给出错误信息:str是错误的参数类型:

而max函数max()可以接收任意多个参数,并返回最大的那个:

四、定义函数

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:。然后,在缩进块中编写函数体,函数的返回值用return语句返回(rreturn可以简单理解为结束一个函数,return跟print可不一样的)。

我们自定义一个求绝对值的my_abs函数为例:

调用my_abs看看返回结果是否正确。

请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回,后续任何语句都不会再执行,简单理解为函数结束指令。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。

如果你已经把my_abs()的函数定义保存为abstest.py文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs来导入my_abs()函数,注意abstest是文件名(不含.py扩展名)。

如果想定义一个什么事也不做的空函数,可以用pass语句:

pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

pass还可以用在其他语句里,比如:

缺少了pass,代码运行就会有语法错误。

上面说了,函数只能返回一个值,如果return语句后面有多个逗号分隔的值,会自动的封包成一个元祖。

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

另外一个函数可以有任意多个return语句,但是始终只会执行一个return语句,执行return语句,会返回到调用方的作用域。如下示例:

说完函数定义的基本语法后,简单说一下如何定义函数帮助,也称为docstring。其实在函数内部可以使用三引号进行编写函数帮助信息,如下:

查看帮助

从Python 3.4开始提供了类型示意的特性,就是可以把帮助信息简单化。语法如下:

意思就是x类型为int,y的类型为int,输出结果也为int。上面都说了是类型示意,也就是没有类型强制,随便输入浮点型、整形、字符型都可以,类型示意目前仅仅作用就是提供帮助信息,帮助IDE做检查。当然,通过这种机制,利用装饰器也可以做类型检查的。

五、参数传递

定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。

Python的函数定义非常简单,但灵活度却非常大。定义函数时可以用位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数,这几种参数可以一起使用,或者只用其中某些。但是请注意,参数定义的顺序必须是:位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数。对于可变的参数需要后置,另外最好可变的参数不和默认参数一起出现。

“位置参数”的传递规则是自左而右,比如执行一个带参数的函数。

对于参数值的传递必须一一对应好函数的参数位置,不能错乱,所以也称之为位置参数。这种方式,当函数的参数过多时,写起来就非常累了,写着写着可能自己都混淆了。所以,Python提供了关键字参数来解决这种烦恼。

“关键字参数”允许你可以按参数名称匹配传递,这样就不需要自左而右传递参数值了,如下:

有了位置参数,我们写和修改起来就好多了。上面说了参数的传递顺序,位置参数必须在关键字参数之前,不然是会抛异常的,如下:

对于位置参数和关键字参数都是“必选参数”,如果有缺少则会抛异常。

定义函数时可以使用默认参数”有默认值的参数且默认参数一定要用不可变对象,如str/none,如果是可变对象运行会有逻辑错误(好处注册信息时符合默认值的就不需要从新填写,而不符合默认值的也可以写)。默认参数最大的好处是能降低调用函数的难度。

调用函数:

从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面)。

默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:

先定义一个函数,传入一个list,添加一个END再返回:

当你正常调用时,结果似乎不错:

当你使用默认参数调用时,一开始结果也是对的:

但是,再次调用add_end()时,结果就不对了:

可能会很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了’END’后的list。

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

所以,定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

现在,无论调用多少次,都不会有问题:

为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

定义函数时使用“*”号加参数名可以定义“可变位置参数”(如果已经有一个list或者tuple,要调用一个可变参数,Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去)

可变位置参数既可以直接传入func(1, 2, 3),也可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

这种写法当然是可行的,也称为参数解构,问题是太繁琐,且元素必须一一对应,不能多也不能少。所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

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

定义函数时使用“**”号加关键字名就可以定义“可变关键字参数”,可变关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict(好处可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用可变关键字参数来定义这个函数就能满足注册的需求)

可变关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{‘a': 1, ‘b': 2})。

在Python中定义函数,可以用位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数,这几种参数可以一起使用,或者只用其中某些。但是请注意,参数定义的顺序必须是:位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数。对于可变的参数需要后置处理,另外最好可变的参数不和默认参数一起出现。

定义一个函数,包含上述几种参数(注意参数顺序):

在函数调用时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。

最后通过一个tuple和dict,你也可以调用该函数。

另外,对于“可变位置参数”,在函数接收到之后,会封包成tuple类型。而“可变关键字参数”在函数接收到之后会封包成dict类型。如下代码示例:

对于函数传参使用起来非常灵活,知识点也多,很多时候可能并不知道有些功能点具体应用场景。目前只需要记住这些功能点即可,慢慢代码写多了,自然也就理解这些功能在实际场景中的应用了。

六、参数检查

调用自定义函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError。

但是如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs和内置函数abs的差别;

当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,所以,这个函数定义不够完善。让我们修改一下my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance实现

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:

七、递归函数

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

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x … x n,用笨方法循环写,如下:

如果用函数fact(n)表示,可以看出:fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

上面就是一个递归函数。可以试试:

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。但是在Python中函数递归还是少用,尽量不用;函数递归执行起来特别慢。

另外在使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000):

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

尾递归是指,在函数返回的时候,调用自身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的fact(n)函数由于return n * fact(n – 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

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

fact(5)对应的fact_iter(5, 1)的调用如下:

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

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

八、生成器函数

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。

下面我们用类似函数的方式定义一个生成器函数,生成器的定义和函数类似,但是有yield语句,如下:

我们看一下类型:

生成器函数返回的是一个生成器,生成器是可迭代的,所以我们可以使用next()方法调用,如下:

generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

前面说过,函数内return是结束一个函数,在生成器函数里面也是一样,当遇到return时,这个函数就被结束,也就意味着生成器被结束。如下在Python 3中执行:

九、nonlocal

前面说了,Python里只有2种作用域:全局作用域和局部作用域。全局作用域是指当前代码所在模块的作用域,局部作用域是指当前函数或方法所在的作用域。其实准确来说,Python 3.x引入了nonlocal关键字,可以用于标识外部作用域的变量。

在Python 2.x中,局部作用域里的代码可以读外部作用域(包括全局作用域)里的变量,但不能更改它。

要在Python 2.x中解决这个问题,目前只能使用全局变量global关键字,但全局变量在任何语言中都不被提倡,因为它很难控制。

为了解决这个问题,Python 3.x引入了nonlocal关键字(详见The nonlocal statement)。只要在闭包内用nonlocal声明变量,就可以让解释器在外层函数中查找变量名了,例如:

完结。。。


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)or分享 (0)
关于作者:

您必须 登录 才能发表评论!