分享

第六章:抽象

 岚风窗 2015-06-15
本章将会介绍将语言组织成函数,这样,你可以告诉计算机如何做事,并且只需告诉一次。有了函数之后,就不必反反复复向计算机传递同样的具体指令了。本章还会详细介绍参数(parameter)和作用域(scope)的概念,以及递归的概念及其在程序中的用途。
一、懒惰即美德
编写一小段代码来计算斐波那契数列(任一个数都是前两个数之和的数字序列)

还可以选择计算前几个数,将用户输入的数字作为动态范围的长度使用,从而改变for语句块循环的次数:

注:读取字符串可以使用raw_input函数,在本例中,可以再用int函数将其转换为整数。
事实上计算斐波那契数列是由抽象的方式完成的:只需要告诉计算机去做就好,不用特别说明应该怎么做,名为fibs的函数被创建,然后在需要计算斐波那契数列的地方调用即可。

二、抽象和结构
抽象可以节省很多工作,实际上它的作用还要很大,它是使得计算机程序可以让人读懂的关键(这也是最基本的要求,不管是读还是写程序)。
程序是非常抽象的,就像“下载网页、计算频率、打印每个单词的频率”一样易懂。

三、创建函数
函数是可以调用(包括包含参数,也就是放在圆括号中的值),它执行某种行为并且返回一个值。一般来说,内建的callable函数可以用来判断函数是否可调用。

注:函数callable在Python3.0中不再可用,需要使用表达式hasattr(func, _call_)代替。

创建函数是组织程序的关键。使用def(或“函数定义”)可以定义函数:

运行这段程序就会得到一个名为hello的新函数,它可以返回一个将输入的参数作为名字的问候语。可以像使用内建函数一样使用它:

再看看返回斐波那契数列的函数:

1、记录函数
如果想要给函数写文档,让后面使用该函数的人能理解的话,可以加入注释(以#开头)。另外一个方式就是直接写上字符串。这类字符串在其他地方可能会非常有用,比如在def语句后面。如果在函数开头写下字符串,它就会作为函数的一部分进行存储,这称为文档字符串。如:

文档字符串可以按如下方式访问:

注:__doc__是函数属性,它的左右两边分别是双下划线。

内建的help函数是非常有用的。在交互式解释器中使用它,就可以得到关于函数,包括它的文档字符串信息:



2、并非真正函数的函数
数学意义上的函数,总在计算其参数后返回点什么。Python的有些函数却并不返回任何东西。没有return语句或者虽有return语句但return后边没有跟任何值的函数不返回值:

这里的return语句只起到结束函数的作用

可以看到,第二个print语句被跳过了(类似于循环中的break语句,不过这里是跳出函数)。
但是如果test不返回任何值,那么x又引用什么呢

所以所有的函数的确都返回了东西:当不需要它们返回值的时候,它们就返回None。
警告:千万不要被默认行为所迷惑。如果在if语句内返回值,那么要确保其他分支也有返回值,这样一来当调用者期待一个序列的时候,就不会意外的返回None

四、参数魔法
函数参数的用法介绍
1、值从哪里来
函数被定义后,能保证函数在被提供给可接受参数的时候正常工作就行,参数错误的话会导致失败。
注:写在def语句中函数名后面的变量通常叫做函数的形式参数,而调用函数的时候提供的值是实际参数,或者成为参数。

2、能改变参数吗
函数通过它的参数获得一系列值。参数只是变量而已,所以它们的行为其实和你预想的一样。在函数内为参数赋予新值不会改变外部任何变量的值:

在try_to_change内,参数n获得了新值,但是它没有影响到name变量。n实际上是个完全不同的变量,具体的工作方式类似于下面:

当变量n改变的时候,变量name不变。同样,当在函数内部把参数重绑定(赋值)的时候,函数外的变量不会受到影响。
赋值对象不一样,结果也不同:


字符串(以及数字和元组)是不可变的,即无法被修改(也就是说只能用新的值覆盖)。所以它们做参数的时候也就无需多做介绍。但是考虑一下如果将可变的数据结构如列表用作参数的时候会发生什么。

本例中,参数被改变了
下面不用函数调用再做一次

当两个变量同时引用一个列表的时候,它们的确是同时引用一个列表。如果想避免这种情况,可以复制一个列表的副本。当在序列中做切片的时候,返回的切片总是一个副本。因此,如果你复制了整个列表的切片,将会得到一个副本:

现在n和names包含两个独立的列表,其值相等:

如果现在改变n,则不会影响到names:

再用change试一下

现在参数n包括一个副本,而原始的列表是安全的
注:可能有的读者会发现这样的问题:函数的局部名称--包括参数在内--并不和外面的函数名称(全局的)冲突。

PS:从本节开始使用ipython,不再使用系统自带的Python。
3、关键字参数和默认值
目前为止我们所使用的参数都叫做位置参数。

两个代码所实现的功能是完全一样的,只是参数名字反过来了,参数的顺序是很难记住的,所以可以直接提供参数的名字:

这样写打印的结果是一样的,因为这里无论怎么写,打印的顺序是先greeting后name的。
但参数名和值一定要对应:

这类使用参数名提供的参数叫做关键字参数。它的主要作用在于可以明确每个参数的作用,也就避免了下面这样的奇怪的函数调用

可以使用

虽然打的字多了些,但是每个参数的含义变得很清晰,就算弄乱了参数的顺序,对于程序的功能也没有任何影响。

关键字参数最厉害的地方在于可以在函数中给参数提供默认值:

当参数具有默认值的时候,调用的时候就不用提供参数了,可以不提供、提供一些或者提供所有的参数:



但是如果只想提供name参数,而让greeting使用默认值怎么办?

注:除非完全清楚程序的功能和参数的意义。否则应该避免混合使用位置参数和关键字参数,一般来说,只有在强制要求的参数个数比可修改的具有默认值的参数个数少的时候,才使用上面提到的参数书写方法。

例如,hello函数可能需要名字作为参数,但是也允许用户自定义名字,问候语和标点:

函数的调用方式很多,下面是其中一些:


注:如果给name赋予默认值,那么最后一个语句就不会产生异常。

4、收集参数
有些时候让用户提供任意数量的参数是很有用的。可以试着这样定义函数:

这里定义了一个参数,前面加上一个*号,让我们用一个参数调用函数看看

可以看到结果作为元组打印出来,因为里面有个逗号,那再看看使用多个参数

参数前的星号将所有值放置在同一个元组中。可以说是将这些值收集起来,然后使用。再试试能不能联合普通参数:


可以使用!所以星号的意思就是“收集其余的位置参数”。如果不提供任何收集的元素,params就是个空元组:

但是能不能处理关键字参数呢?

不行!所以我们需要另外一个能处理关键字参数的“收集”操作。


返回的是字典而不是元组,放一起用看看:


回到原来的问题,如何实现多个名字同时存储:


5、反转过程
函数收集的逆过程,先看以下函数

比如说有个包含由两个要相加的数字组成的元组:

这个过程有点像上个方法的逆过程。不是要收集参数,而是分配它们在“另一端”。使用*运算符就简单了——不过是在调用而不是在定义时使用:

可以使用同样的技术来处理字典——使用双星号运算符。


在定义或者调用函数时使用星号(或者双星号)仅传递元组或字典,所以可能没遇到什么麻烦:



可以看到在with_stars中,在定义和调用函数时都使用了星号。而在without_stars中两处都没用,但是得到了相同的效果。所以星号只在定义函数(允许使用不定数目的参数)或者调用(“分割”字典或者序列)时才有用。

提示:使用拼接(Splicing)操作符“传递”参数很有用,因为这样就不用关心参数的个数之类的问题,如:

在调用超类的构造函数时这个方法尤其有用。

6、练习使用参数
我们把以上用的方法放在一起举个例子,以便区分:


让我们试一下:





定义的第二个power函数打印结果如下:

所以可以把程序修改如下:




为了更深入的理解,提供下面几个代码练习:





五、作用域
变量和所对应的值用的是一个“不可见”的字典。内建的vars函数可以返回这个字典:

警告:一般来说,vars所返回的字典是不能修改的,因为根据官方的Python文档的说法,结果是未定义的。换句话说,可能得不到想要的结果。

这类“不可见字典”叫做命名空间或者作用域。除了全局作用域外,每个函数调用都会创建一个新的作用域:

赋值语句x = 42只在内部作用域(局部命名空间)起作用,所以它并不影响外部(全局)作用域中的x。函数内的变量被称为局部变量——这与全局变量的概念相反。参数的工作原理类似于局部变量,所以用全局变量的名字作为参数名并没有问题

如果需要在函数内部访问全局变量,而且只想读取变量的值(也就是说不想重新绑定变量),一般来说没有问题

警告:像这样引用全局变量是很多错误引发原因。慎重使用全局变量。

屏蔽的问题
如果局部变量或者参数的名字和想要访问的全局变量名相同的话,就不能直接访问了,全局变量会被局部变量屏蔽。
如果确实需要的话,可以使用globals函数获取全局变量值,这函数的近亲是vars,它可以返回全局变量的字典。例如

如果在函数内部将值赋予一个变量,它会自动成为局部变量——除非告知Python将其声明为全局变量,接下来讨论重绑定全局变量(使变量引用其它新值)。


嵌套作用域:
Python函数是可以嵌套的,可以将一个函数放在另一个函数里面


如果需要用一个函数“创建”另一个,嵌套显得很有用

一个函数位于另外一个里面,外层函数返回里层函数。也就是说函数本身被返回了——但是没有被调用。重要的是返回的函数还可以访问它的定义所在的作用域。
每次调用外层函数,它内部的函数都被重新绑定,factor变量每次都有一个新的值。由于Python的嵌套作用域,来自multiplier的外部作用域的这个变量,稍后会被内层函数访问

类似multiplyByFactor函数存储子封闭作用域的行为叫做闭包。

六、递归
递归简单的说就是引用(或者调用)自身的意思。
1、两个经典:阶乘和幂
阶乘:

用定义函数的方法很简单


幂:

或者


2、另外一个经典:二元查找


Python在应对“函数式编程”方面有一些有用的函数:map、filter和reduce函数。
使用map函数将序列中的元素全部传递给一个函数:

filter函数可以基于一个返回布尔值的函数对元素进行过滤

本例使用列表推导式可以不用专门定义一个函数:

还有个叫做lambda表达式的特性可以创建短小的函数:

如果需要计算一个序列的数字的和,可以使用reduce函数加上lambda  x,y:x+y


七、小结
抽象:抽象是隐藏多余细节的艺术。定义处理细节的函数可以让程序更抽象。
函数定义:函数使用def语句定义。它们是由语句组成的块,可以从“外部世界”获取值(参数),也可以返回一个或者多个值作为运算的结果。
参数:函数从参数中得到需要的信息--也就是函数调用时设定的变量。Python中有两类参数:位置参数和关键字参数。参数在给定默认值时是可选的。
作用域:变量存储在作用域(也叫做命名空间)中。Python中有两类主要的作用域--全局作用域和局部作用域。作用域可以嵌套。
递归:函数可以调用自身--如果它这么做了就叫做递归。一切用递归实现的功能都可以用循环实现,但是有些时候递归函数更易读。
函数型编程:Python有一些进行函数型编程的机制。包括lambda表达式以及map、filter和reduce函数。


























    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多