Python 6 函数基础理论

作者 柚爸

本次教学博客地址
函数的作用是非常重要的,函数可以说是现代编程中最小的模块单元.在以前自学的时候,感觉总是学的不透,这次听讲看看,是不是可以从全新的视角来看待函数.

什么是函数

函数本质上是子程序,就是可以独立执行一个功能的程序.
函数的操作方法是将函数体压入栈中,然后传入参数,在计算完毕之后,将return的值返回到调用处,然后将函数从栈中释放出去.
函数对于编程的一般意义来讲,是实现一个特定的功能,并且可以反复调用,减少代码量.
一般我们称不返回结果的函数叫做过程,返回函数值的叫做过程.从根本上来讲,这和计算机程序设计为不断的求值表达式的根本结构相关.SICP的开头的函数式编程介绍就非常突出这一点.虽然很多语言没有像函数式语言意义突出函数式编程范式的特点,但是底层的设计都是类似的.
在python里没有return语句的话会返回None,所以python里没有真正意义上的过程.所有的都是函数.
面向对象其实是在函数上发展起来的,对象的方法其实就可以看做函数.当然函数在面向对象的语言中也是函数,可以认为面向对象语言是将函数,基础数据类型这些东西做了一个更高层次的封装.
函数在python里,一旦定义,就有一个名称对应到函数这块内存空间里,函数对象与数值,列表,集合,字典,元组一样,属于python的一级对象,可以自由的通过变量传递.

函数的格式

函数首先要有名称,函数的名称必须符合python标识符的规定.此外在函数名之外,需要用小括号定义函数的参数.
在函数体内可以写任意代码,当然这里的变量涉及到作用域.但一般要先用三个双引号引起来写函数文档,这是一个非常好的习惯.
返回值,用return规定.函数一旦执行到return语句,就会结束执行.return的值超过一个的时候,是返回一个包含各个对象的元组.

函数的参数

函数的参数是函数的重要组成部分,函数的功能建立在合理的参数传入上.
函数的参数在定义的时候叫做形参,只是一个名称,在函数体编写代码时,如果要使用该参数,则需要按照定义的名称使用.在传入参数的时候,参数对应的基本数据类型或者引用,是被复制一份传入函数的.

定义参数

函数的定义和调用的方法很灵活,所以通过定义和传入来分开说.
def myfunction(x,y=1,*args,**kwargs)
其中x叫做普通位置参数,在传入的时候,x对应的值是按值顺序从传入的值中获取的.
y=1 是默认值参数,如果不传入y,则会由默认值替代,如果传入,则按照传入的值.
*args 是以元组的形式接受一系列不固定数量的位置参数.
**kwargs 是以字典的形式接收一系列不固定数量的关键字参数.
*args与**kwargs合用,则可以让一个函数接受任意自变量.*args一定要在**kwargs之前.
注意,在定义的时候加*和**,和在传入参数的时候加*和**,是不同的.定义的时候表示是用元组或者字典将传入的数据来进行组合,在传参数的时候用*和**表示拆解开序列或者字典的元素赋值个各个变量.
如果定义了一个**kwargs,传入的时候也用**dict,则会将整个字典的内容拆解到kwargs这个字典里.*和**的定义参数方法,是为了让函数有灵活的扩展,以及方便传入参数.
还有一个需要注意的地方,就是如果默认值指定了一个引用,这个引用一旦创建,会被反复引用,所以一般不指定引用的默认值,而是在函数体内创建然后执行操作后返回,下一次调用该函数时,由于引用名是局部变量,所以不会和上一次执行函数的时候相同.
函数定义的时候,位置参数一定要在关键字参数左边.

传入参数

传入参数其实本质上就分两种,位置参数和关键字参数.
传参的时候要和定义结合起来看,位置参数按照位置拆解.关键字参数直接赋值,但绝对不能先覆盖关键字参数的位置,再按照关键字方法传入参数,这样会导致同一变量多次传参.最后是需要收集的部分.
用*和**拆解序列和字典的时候,是可以一次性将所有参数按照位置或者关键字方式传入.
定义了*和**之后,是传入的参数被收集打包成序列和字典.这两种方法要灵活运用.只要记得*接受单个任意数量位置参数,而**接受任意数量的关键字参数.

变量的作用域

这里简单的先提一下,作用域是个比较复杂的东西.
全局变量就是写在任何代码块之外的变量,这个变量实际上是在当前模块(.py文件)里是全局变量.
函数体内的变量,包括定义函数时候的形参,他们的作用域仅限在函数体内,如果有子函数,外部函数的形参对于子函数来讲也是全局变量.
在子程序内,如果普通重新定义与全局变量相同的名称,则在作用域内会将全局变量覆盖.如果要修改全局变量,则先用一行global 变量名定义,之后针对该变量操作都是针对全局变量了.
函数的作用域在创建的时候就确定了,采用global这种方式,只是能让你在子程序中操作全局变量,但不会改变全局变量的作用域.
不过这里一定要注意,如果引用全局可变对象,也就是按引用传递,最好将引用复制一份,否则对全局可变对象直接操作,会修改全局可变对象..这块自己学过C语言,也是这样的,因为传递的是指针,不是变量的内容.这里实际上就是值传递和引用传递的区别.
注意:不管几层嵌套,global 声明始终指的是写在模块最外边的全局变量,如果想指定上一层的局部变量,用nonlocal声明.

递归

SICP前边几页的递归讲的不错,推荐给希望了解递归的人去看一看,scheme写递归的思路特别清晰,可以看出来嵌套关系,顺便也讲了尾递归,就是修改函数每次用一些变量来传递上一次函数的状态,这样便不用反复将函数体压栈.
函数调用自身的过程,就是递归,递归用来解决问题可以让思路比较清晰,但是执行的效率比较低下,因为函数是用栈来运行的,每一次递归便要将函数体压入栈中,如果递归次数过多则内存可能不够用.
递归需要设置明确的结束条件,才能够正常结束.

Pycharm里可以设置断点,然后用debug运行的方式来查看程序运行的过程.这点新学到.
递归其实还是要找题目练习,以及学习很多规划分析的方法.

函数作用域

函数的作用域与它所使用的参数有关,函数的作用域就涉及到闭包.
闭包的定义:如果在一个内部函数里,对在外部函数内(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure).
实际上可以认为,闭包函数不仅是定义了函数体,还将函数调用的参数,也就是数据打包在了一起.在函数柯里化的概念影响下也就产生了闭包.闭包一般是要返回内部函数.在实践中如何使用闭包函数,还有待进一步学习.

匿名函数lambda

Lambda, Λ, λ,是希腊字母.在python里,用lambda表达式创建一个匿名函数,在需要用到函数的地方可以简单当做一个函数对象使用,而无需创建正式的函数对象.
lambda 变量名:返回值
lambda的实质就是仅写下来函数体,因为函数名只是一个名称,而函数体如何处理参数才是实际的存在,那么如果去掉函数名,依然可以表达整个函数,这就是lambda表达式.
lambda返回多个值的时候,Python不会帮你自动将结果组成元组,需要手工写.在正式函数return 的时候是可以采取 return x,y,z 这种写法的.

高阶函数

高阶函数两个特点:传入的参数是函数,返回的结果是函数.
编程中高阶函数的概念来自于函数式编程,在函数式编程中是没有变量的,必须函数嵌套使用,即函数将自己的结果返回给其他函数继续使用,一层一层连续使用函数.
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回函数.前边的闭包就是用到了python的高阶函数特性.
由于python支持变量,即在传递函数的过程中,可以用变量存储,因此不是纯粹的函数式编程语言.
这里就要提一下函数式编程:

函数式编程:函数式=编程语言定义的函数+数学意义的函数
通俗来讲,函数式就是用编程语言去实现数学函数。这种函数内对象是永恒不变的,要么参数是函数,要么返回值是函数,没有for和while循环,所有的循环都由递归去实现,无变量的赋值(即不用变量去保存状态),无赋值即不改变。
函数式编程一个写的比较通俗的介绍可以看这里.
函数式编程的语句比较精简,但是可读性很差.但是函数式编程对于理解编程思想有着非常大的帮助,近些年来函数式编程又逐渐被人们拾起.有兴趣的人可以就按照SICP用scheme写写看.

理解了可以将函数本身作为参数,那么python里有些函数就可以将函数作为参数,用参数函数去执行一些功能,得到结果以后,再对这些结果进行处理.

尾递归:函数执行的最后一步(不是最后一行,而是最后执行的步骤),进入递归,就是尾递归.如果是尾递归,则函数的当前状态可以不保留,只需要将结果传入新函数即可,正常的编译器优化,这种递归方式不会额外耗用内存.
尾调用的做法类似,即函数的最后一步调用其他函数,则当前函数可以全部释放.

讲了很多理论,现在看看python里利用到高阶函数特性的主要函数:

map(func, iterables)将iterable内的每个元素传入func作为参数计算结果,得到结果的生成器.
filter(function or None, iterables),用function函数判断iterables里的每个元素,如果为True,则保留该元素,为False,则不保留该元素.最后得到所有True的元素的生成器.如果不传function,则条件为True
reduce(function,sequence,initial= None),用一个能够接受2个参数的函数function,将sequence这个序列里的元素从左到右不断操作,第一次用头两个变量的结果,然后用第一次操作的结果和第三个元素继续来用function操作.如果指定了initial,则把initial这个元素加到sequence序列的最左边,然后对这个新序列应用function.返回的是计算后的结果,像map和filter返回生成器.python2.7里reduce可以直接使用,python3里需要从functools模块内导入.

函数的基本理论学习完了,剩下的就是在实践中练习了.