首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

第6章-分治策略-函数和模块

第6章分治策略-函数与模块

本章大纲

6.1 函数基础

6.2 变量作用域

6.3 参数的类型

6.4 内建函数

6.5 匿名函数

6.6 yield

6.7 模块和包

为了解决更多的问题或实现更多的功能,我们的代码量也开始直线上升,其中可能会出现一些功能重复或类似的代码,但是,大量重复功能代码堆积并不会让你看起来是个高手。

分治策略是基于“分而治之”的思想,在开发一个比较大的程序的时候,最好的办法是基于较小程序组件来构建。较小的程序组件更灵活,程序更易于开发和维护。Python中的程序组件包括函数、类、模块和包。本章我们就先来说一下函数和模块。

6.1函数基础

函数其实就是一段具有特定共功能、被封装、可重用的语句块,通常用来实现某一个特定的功能。给这段程序起一个名字,就可以在程序的任何地方通过这个名字任意多次的运行这个语句块。这也是函数的两个重要概念,定义和调用。

函数的工作常态就是接收数据,在内部进行处理,返回处理结果,有个有意思的比喻用来形容这样的模型叫黑箱模型。这是个比较形象的比喻,指一段封装了特定功能的程序,对于需要这种功能的用户来说,并不需要知道内部实现的原理和过程,程序本身提供了输入的接口,经过内部处理后对用户返回结果。黑想模型在编程领域非常常见,包括人工智能其实也是黑箱模型。

函数可以简化脚本,在我们前面的学习过程中,已经接触并运用了一些Python提供的内建函数。我们并不需要了解函数内部怎样实现的,只是拿来一个名字就可以实现很多看似复杂的功能,大大的减少了程序代码的复杂度。

除了Python提供的内建函数,我们也可以根据需要编写自己的函数,通常用来完成大量重复或特定的功能,这跟循环不一样,循环是一次性反复执行一段代码,函数可以在需要的时候随时被多次调用。

6.1.1自定义函数

自定义函数通过def关键字定义。def 关键字后就是函数的标识符也就是函数名,函数提供的输入接口就是函数名后面的圆括号,其中是变量名,在函数中我们称之为参数,一个函数的参数数量视函数功能决。圆括号后以冒号结尾,接下来就是函数的语句块了,函数语句块相对于def要缩进,具体格式如下:

def函数名(参数):

语句块(函数体)

我们先看一个无参数示例,把之前的99乘法表放到函数里,这样就可以随时打印99乘法表了,控制篇幅,我们打印到5:

这个例子定义了一个名字叫fun99的函数,括号中为空的意思是不需要参数,最终功能就是函数体的代码功能。调用函数的方法就是在需要执行的地方通过函数名加圆括号就可以了。运行效果如下:

6.1.2形参和实参

自定义函数,作为一个工具可能会在不同的情况下调用,而且,可能会需要根据情况给出不同的结果,比如我就想打印3*3乘法表,或打印100*100的乘法表,这时就需要参数,参数作为函数的输入,由调用者决定传入函数不同的值,经过函数处理后返回对应的结果。

定义函数时,在函数名后圆括号内的参数的叫做形参,如果有多个形参,需要通过逗号隔开,这个形参并不是具体的值,其实就相当于一个变量名。

调用函数的时候可以通过参数给函数传值,通过参数赋值的过程叫传参。而调用函数时这个参数就叫实参,通常为了交流方便,并不会特意说形参或实参,而直接说参数。

现在我们先改造一下乘法表的函数,通过参数来控制打印的具体值:

这里我们设置了一个形参x,当调用这个函数的时候就需要给x传值,这个只会传递到函数内部,有参数函数调用时需要注意的是实参的数量要与形参相同,不传参或多传参都会产生异常,效果如下:

关于参数的问题我们在第三小节集中介绍

6.1.3返回值

刚刚的例子中我们都是通过print()来直观的看到函数的执行的效果,但我们在使用过的内建中,大部分都会返回一个结果,而并不是打印到屏幕上。这些返回的结果就是函数的返回值,可以赋值给一个变量,或者作为其他表达式的一部分。

返回值是函数的一部分,即使你不设置,依然也是有返回值的。比如:

效果如下:

因为没有设置返回值,所以看到的是None。

自定义返回值用return语句。return语句后面是一个表达式,这个表达式可以很复杂也可以直接就是一个值。调用函数相当于进入函数,执行到return语句从函数中返回,同时表达式的值作为返回值。

下面是一个能够返回给定半径的圆的面积的函数:

每个函数只会有一个返回值,如果有多个return语句,只会执行第一个。但是我们可以设计一些分支来返回不同的返回值,比如下面这个获得绝对值的函数:

需要注意的是,return语句一旦执行,函数就会终止,其后的其他语句都不会执行。

另外,Python中提供了一个内建abs,用来计算绝对值。这里要说的就是程序员中的一个共识,叫不要重复造轮子。简单说就是已经存在的功能就直接拿来用,练习是练习,工作中不要什么都自己写,费时费力还不高效。

6.2变量作用域

变量作用域指的是变量起作用的范围,涉及到函数编程就会有变量作用于的问题。比如我们在函数内部定义的变量和函数外部定义的变量,作用域就有区别了。

6.2.1局部变量

局部变量是我们在函数内定义的变量,这种变量也只能在函数内部使用。局部变量的作用域就只在它被定义的语句块中。

下面的例子用来说明局部变量:

在这个例子中,我们在主程序中定义了变量x的值为10,定义函数的时候,在函数中也定义了一个变量x,值是100,当然,这两个变量没有任何关系,另外我们还定义了一个变量y。当我们调用函数时将主程序的x的值传给函数,函数内打印过一次之后,将x的值重新定义为100,之后再打印一次。当函数调用完毕后再打印一次主程序中x的值。可以看到主程序的变量并没有被函数内的赋值所影响。而y的值只在函数内可以被调用,在函数之外则不存在。运行结果如下:

6.2.2全局变量

如果想要函数内的变量作用于函数之外,这种变量的作用域就必须是全局的。能够作用与函数内外的变量,叫做全局变量。

全局变量可以通过global语句定义,我们改造一下上面的程序,把x和y定义成全局变量:

运行结果如下:

在这个例子中,我们在主程序定义过的x,在函数内部生命了全局变量后,调用了函数之后,x的值也跟着变了。主程序没定义过的y,在函数内部声明为全局变量后,在函数调用之后,也可以在主程序中调用了。

6.2.3命名空间

Python中有着严格地变量作用域的区分,主要依据就是变量定义的位置,也就是命名空间。Python的命名空间一共有三个:局部、全局和内建。程序访问变量的时候会安局部、全局、内建的顺序搜索命名空间。

比如我们在一个函数中访问一个变量的时候,就会先看这个函数中是否有这个变量,没有就会再搜索全局,如果还没有就会搜索内建命名空间。不同函数拥有独立的命名空间,也就是不同函数中名字相同的变量并不会相互影响。

了解了命名空间,就有一个问题需要注意了,就是尽量避免同名的全局变量、局部变量以及函数的参数定义。因为。,很容易会相互影响,而且,代码也不易阅读。

6.3参数的类型

在第一小节中,我们说到了传参可能会遇到的问题,有参数不传参、多传或少传等都会产生异常,这一节我们就来看看怎么解决这些问题。

6.3.1默认参数

函数设置了形参后,如果不传参就会产生异常,不过,我们可以通过给参数设置默认值来解决这个问题。参数设置了默认值后,在调用函数的时候如果不传参,就会使用默认值,如果有实参,则将实参重新传递给形参,设置默认参数的方法就是在定义函数的时候,通过等号为形参直接赋值。

下面看一个小例子:

在这个例子中的两个参数都设置了默认值,调用时如果不传参则使用默认值,如果实参传值则用新值。效果如下:

如果有多个形参的话,就需要特别注意,因为函数调用时,参数赋值默认是从左至右依次赋值的。所以,如果多个形参中有默认参数的话要从右至左设置,不可以先声明默认参数,在右侧再出现没有默认值的形参,比如下面几种形式:

6.3.2关键参数

如果函数中有多个参数,可以通过参数名字名字作为关键字给参数赋值,这时可以不按顺序赋值。例如:

在这个例子中,我们跳过food指定了money和other的值,效果如下:

关键参数的好处就是,可以不按顺序传值。

6.3.3冗余参数处理

传值少可以通过默认值解决,但有的时候某些函数被调用的时候,传递参数的个数就是要比形参多,比如这样调用上面例子的函数:

就会看到这样的异常:

异常提示说的很清楚,这个函数只需要1到3个值,但是给了5个。这种情况的解决办法就是定义函数的时候设置可变长的的参数。通过在参数前加“*”即可实现。

我们在other前添加了“*”标识符,因为传参顺序的原因,混合普通参数和可变长度参数的时候,可变长度参数放在最右边。

效果如下:

我们可以看到,有了可变长度参数之后,多余的值被以元组的形式保存起来。

另外对于关键字传值,需要给形参加“**”标识符,如:

效果如下:

这时,多余的参数被以字典的形式保存下来。

有了这么多的参数处理形式,在实际项目中就可以灵活运用来实现不同的功能了。

6.3.4序列和字典做实参

如果我们将序列和字典作为形参传递给函数会怎么样呢?先看个小例子:

结果如下:

很明显,列表也好,字典也好,都是作为一个整体传给了函数,但是如果像下面这个函数这样,正好需要三个值,那我们就可以在调用函数时通过实参传值时把他们分解开来,方法是通过加“*”:

需要注意的是,拆分列表用“*”,拆分字典用“**”,并且字典的key要跟函数的形参一致。

6.4内建函数

Python中提供了大量的内建函数,程序员流行一句话,不要重复造轮子,意思是说,已经存在了相应功能的函数,就不要自己定义了。有些功能在实现之前可以先看看是否已经存在相关函数,比如len()、max()、abs()等等,手册有详细说明。。

图 Python3 内建函数

这个小节我们了解几个有意思的内建函数。

1.filter

filter(function, iterable):筛选过滤,循环可迭代的对象,把迭代的对象当作函数的参数,如果符合条件就返回True,否则就返回False。

我们来过滤一个掉一个列表中的所有偶数:

运行效果如下:

filter这样的函数有个特别的地方就是第一个参数必须是一个函数,刚开始接触会有点不适应,在手册的说明中也会有相应的说明,具体应用的时候只要把手册的说明看明白,按手册的要求却用就可以了。

2.map

map(function, iterable, ...):将序列中的每一个元素都传到函数中执行并返回,可以同时遍历多个序列,如果长短不一,以短序列为准。

下面将两个序列的元素分别相加;

map也用到了函数参数,结果如下:

因为取短的原因,所以,虽然第二个序列有4个值,但结果只有3个。

6.5匿名函数-lambda表达式

lmabda表达式的作用是实现一个轻便的函数功能,又不需要起名字,所以也叫匿名函数。

什么时候会用到函数却不需要名字呢?如果你观察上一小节的两个例子应该会发现,其中的函数参数都是为filter和map定制的,其实也只有在filter和map中才有用,这种函数我们就可以用lambda表达式来代替。

lambda表达式的语法是:

lambda 参数:返回值表达式

前面用于filter的函数可以写成:

这只是一个表达式,并不能被调用,因为没有名字。如果想调用的话可以起个名字,不过,这样做的意义不大,这也违背了lambda表达式的初衷。如:

上一小节的filter的例子就可以写成这样:

map的例子更改如下:

有了匿名函数就可以使程序更简洁,但并不是不分情况的乱用,比如这两个例子还有一个特点,都是通过对序列遍历生成新的序列或迭代对象,这个特点正是列表解析和生成器表达式可以做的事。所以,这两个例子还可以写成列表解析的形式。

用列表解析改写filter的例子如下:

列表解析改写map的例子如下:

这里用了一个zip,zip有什么用?打开电脑自己查手册看看吧。

虽然这几个例子可以互相转化,但其实这只是因为这几个办法正好都可以用来解决问题而已,并不是lambda跟列表解析存在必然联系。实际在设计程序的时候,根据上下文,选择合适的方式就可以了。

6.6生成器yield语句

函数可以通过return返回一个值,但是通过yield语句却可以让函数分多次返回多个值。

简单的说,生成器就是包含yield关键字的函数。本质上来说,关键字yield是一个语法糖,内部实现支持了迭代器协议。同时yield内部是一个状态机,维护着挂起和继续的状态。

那么,生成器是怎么调用执行的呢?只需要了解下面几条规则即可:

当生成器被调用的时候,函数体的代码不会被执行,而是会返回一个迭代器。其实,生成器函数返回的是生成器的迭代器。 “生成器的迭代器”这个术语通常被称作”生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法,其中一个就是next()。如同迭代器一样,我们可以使用next()函数来获取下一个值。这一切都是在yield内部实现的。

当next()方法第一次被调用的时候,生成器函数才开始执行,执行到yield语句处停止。next()方法的返回值就是yield语句处的参数(yielded value)。当继续调用next()方法的时候,函数将接着上一次停止的yield语句处继续执行,并到下一个yield处停止;如果后面没有yield就抛出StopIteration异常。

每调用一次生成器的next()方法,就会执行生成器中的代码,直到遇到一个yield或者return语句。yield语句意味着应该生成一个值(在上面已经解释清楚)。return意味着生成器要停止执行,不在产生任何东西。

生成器的编写方法和函数定义类似,只是在return的地方改为yield。生成器中可以有多个yield。当生成器遇到一个yield时,会暂停运行生成器,返回yield后面的值。当再次调用生成器的时候,会从刚才暂停的地方继续运行,直到下一个yield。生成器自身又构成一个循环器,每次循环使用一个yield返回的值。

我们来看一个比较直接的例子:

运行结果如下:

从结果我们就可以直观看到效果了,含有yield语句的函数被调用后就会返回一个生成器对象,前两次通过next函数都可以取到yield返回的值,第三次next函数在执行完函数体代码后因为没有yield了,所以抛出了StopIteration异常。

6.7模块和包

通过函数我们可以把的问题分解为若干的小问题,并且可以让程序更灵活,不过有的时候你可能在一个项目中有一些函数或类,在几个程序中都会用到,这时我们就要通过模块或者包来达到代码重用的目的了。

6.7.1模块

Python 模块(Module),其实就是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和Python语句。换句话说,其实每个Python脚本都可以被当做模块。

模块化编程的好处就是可以让你能够有逻辑地组织你的 Python 代码段。并且把相关的代码分配到一个模块里能让你的代码更好用,更易懂。在模块中能定义函数,类和变量,模块里也能包含可执行的代码。

下面的脚本就可以被当作一个有点用模块:

6.7.2导入模块

导入模块的方式就是通过import语句或from…import语句,在一个程序中每个模块只需要导入一次。import语句的语法为:

要调用模块中的方法,在模块导入后,需要“模块名.函数名”的方式。

import语句我们在之前的演示中已经用过很多次了,现在要导入我们自己写的模块。我们可以这样导入前面的support.py模块,并使用里面的方法:

第一次到如自己写的模块时需要注意,当解释器遇到 import 语句,如果模块在当前的搜索路径就会被导入, 否则会失败。搜索路径是一个解释器会先进行搜索的所有目录的列表。如果你还不清楚搜索路径,可以将support.py和test.py放到同一个目录下。

如果一个模块中有很多函数,但是只需要用到一个或者几个,就需要用from语句了,语法如下:

比如现在导入support模块的myAdd函数,并调用:

通过这种方式导入的函数可以直接调用,如果你觉得这种方式更方便,可以通过下面的语句导入所有函数:

虽然这样导入后,在调用函数的时候会感觉方便些,但却并不建议,因为很有可能跟当前程序中的函数弄混。

最后还有一个小技巧,就是如果导入的模块名字比较长,可以通过取别名的方式简化。名字可以自己随便起,一些比较常用的模块都有公认的别名,比如numpy就通常都起名叫np,这样别人在看代码的时候也第一时间就能反应过来你的别名代表的是什么。

6.7.3搜索路径

Python在导入模块的时候,会按顺序自动搜索模块,搜索过程如下:

1.当前目录

2.当前目录没有,则搜索shell变量PYTHONPATH下的所有目录。

3.如果都找不到,Python会察看默认路径。UNIX/Linux下,默认路径一般为/usr/local/lib/python/;Windows通常在c:python27lib(取决于你的安装路径)。

6.7.4包

模块通常是包含了很多函数或类的一个脚本,而包则可以理解为是包含了很多模块的一个目录(文件夹)。

包是一个分层次的文件目录结构,它定义了一个由模块及子包,和子包下的子包等组成的 Python 的应用环境。

其实包就是一个文件夹,但该文件夹下必须存在 __init__.py 文件, 用于标识当前文件夹是一个包。该文件的内容可以为空。你可能会发现,没有__int__.py,你的包一样可以用,但是别忘了,当别人来看你的代码结构时,如果没有__int__.py,别人是不太可能第一时间就知道你这目录是个包的。

现在我们把前面的support.py放到文件夹milo里,并在这个milo文件夹里建一个空文件__init__.py。这时的milo就是一个包了。目录结构如下:

test.py是我们用来测试包的脚本。

导入模块跟导入包类似,只是结构上多了一级我们在导入的时候注意包的结构就可以了,比如下面这些方法都是可以的:

6.7.5__name__属性

Python内置的__name__属性可以用来识别程序是被导入的还是直接执行的。因为文件如果是以顶层程序文件(主程序)执行,__name__值为“__main__”, 如果文件是被导入,__name__的值就是当前脚本的名字。

如果我们编写的模块中有需要执行的程序,而这些程序我们并不想让调用者执行,那就需要__name__了。原理很简单,我们先看下__name__在两种情况下的值。

分别运行support.py和test.Py,结果如下;

可以看到,在模块中我们打印了__name__的值,直接运行就结果就是“__main__”,但是在test.py中我们只是导入了模块,也打印了一个字符串,就是模块文件的名字。这是因为,我们在导入模块的时候,实际上就是加载了一次模块当中的代码,所以,模块中的print()被执行,但此时的__name__值就变成了模块文件的名字。根据这个功能,我们就可以避免模块中的执行语句被倒入这只行了:

“if __name__ == '__main__':”的用法还是非常常用的,特别是你的脚本中可被调用的函数或类的时候,即便没人调用,通常习惯上也会这样执行主程序。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180417G1O97M00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券