Python基础及应用
上QQ阅读APP看书,第一时间看更新

3.4 函数的定义与使用

还记得第2章提到过的一个“内置函数”max吗?对于不同的List和Tuple,这个函数总能给出正确的结果——当然有人说用for循环实现也很快很方便,但是有多少个List或Tuple就要写多少个完全重复的for循环,这是很让人厌烦的,这时候就需要函数出场了。

本章会从数学中的函数引入,详细讲解Python中函数的基本用法。

3.4.1 认识Python的函数

函数的数学定义为:给定一个数集A,对A施加对应法则f,记作fA),得到另一个数集B,也就是B=fA),那么这个关系式就叫函数关系式,简称函数。

数学中的函数其实就是AB之间的一种关系,我们可以理解为从A中取出任意一个输入都能在B中找到特定的输出,在程序中,函数也是完成这样的一种输入到输出的映射,但是程序中的函数有着更大的意义。

它首先可以减少重复代码,因为我们可以把相似的逻辑抽象成一个函数,减少重复代码,其次它有可以使程序模块化并且提高可读性。

以之前多次用到的一个函数print为例:

由于print是一个函数,因此我们不用再去实现一遍打印到屏幕的功能,减少了大量的重复代码,同时看到print就可以知道这一行是用来打印的,可读性自然也提高了,另外如果打印出现问题只要去查看print函数的内部就可以了,而不用再去看print以外的代码,这体现了模块化的思想。

但是,内置函数的功能非常有限,我们需要根据实际需求编写自己的函数,这样才能进一步提高程序的简洁性、可读性和可扩展性。

3.4.2 函数的定义和调用

1.定义

和数学中的函数类似,Python中的函数需要先定义才能使用,比如:

这是一个基本的函数定义,其中第1、4、6行是函数特有的,其他我们都已经学习过了。

先看第1行:

这一行有四个关键点:

def:函数定义的关键字,写在最前面。

ask_me_to:函数名,命名要求和变量一致。

(string):函数的参数,多个参数用逗号隔开。

● 结尾冒号:函数声明的语法要求。

然后看第2到第5行:

它们都缩进了四个空格,意味着它们构成了一个代码块,同时从第2行可以看到函数内是可以接着调用函数的。

接着再看第4行:

这里引入了一个新关键字:return,它的作用是结束函数并返回到之前调用函数处的下一句。返回的对象是return后面的表达式,如果表达式为空则返回None。第6行跟第4行功能相同,这里不再赘述。

2.调用

在数学中函数需要一个自变量才会得到因变量,Python的函数也是一样,只是定义的话并不会执行,还需要调用,比如:

注意这里是两个函数嵌套,首先调用的是我们自定义的函数ask_me_to,接着ask_me_to的返回值传给了print,所以会输出ask_me_to的返回值:

定义和调用都很好理解,接下来了解函数的参数怎么设置。

3.4.3 函数的参数

Python的函数参数非常灵活,我们已经学习了最基本的一种,比如:

它拥有一个参数,名字为string

函数参数的个数可以为0个或多个,比如:

我们可以根据需求去选择参数个数,但是要注意的是,即使没有参数,括号也不可省略。

Python的一个灵活之处在于函数参数形式的多样性,有以下几种形式。

● 不带默认参数的:deffunc(a)

● 带默认参数的:deffunc(a,b=1)

● 任意位置参数:deffunc(a,b=1,∗c)

● 任意键值参数:deffunc(a,b=1,∗c,∗∗d)

第一种就是我们刚才讲到的一般形式,下面介绍剩下三种如何使用。

3.4.4 默认参数

有时候某个函数参数大部分时候为某个特定值,于是我们希望这个参数可以有一个默认值,这样就不用频繁指定相同的值给这个参数了。默认参数的用法看一个例子:

这是一个格式化输出日期的函数,注意其中的月份和天数参数,用一个等号表明赋了默认值。于是可以分别以1,2,3个参数调用这个函数,同时也可以指定某个特定参数,比如:

这段代码会输出:

我们依次看一下这些调用:

1)print_date(2018)这种情况下由于默认参数的存在等价于print_date(2018,1,1)。

2)print_date(2018,2,1)这种情况下所有参数都被传入了,因此和无默认参数的行为是一致的。

3)print_date(2018,5)省略了day,因为参数是按照顺序传入的。

4)print_date(2018,day=3)省略了month,由于和声明顺序不一致,所以必须声明参数名称。

5)print_date(2018,month=2,day=5)全部声明也是可以的。

使用默认参数可以让函数的行为更加灵活。

3.4.5 任意位置参数

如果函数想接收任意数量的参数,那么可以这样声明使用:

诊断代码会输出:

任意位置参数的特点就是它只占一个参数,并且以 ∗ 开头。其中args为一个List,包含了所有传入的参数,顺序为调用时候的传参的顺序。

3.4.6 任意键值参数

除了接受任意数量的参数,如果我们希望给每个参数一个名字,那么可以这么声明参数:

这段代码会输出:

跟之前讲过的任意位置参数使用非常类似,但是kwargs这里是一个Dict(字典),其中Key和Value为调用时候传入的参数的名称和值,顺序和传参顺序一致。

3.4.7 组合使用

我们现在知道了这四类参数,它们可以同时使用,但是需要满足一定的条件,比如:

可以看出,四种参数在定义时应该满足这样的顺序:非默认参数、默认参数、任意位置参数、任意键值参数。

调用的时候,参数分为两类:位置相关参数和无关键词参数,比如:

这句代码会输出:

其中前三个就是位置相关参数,最后一个是关键词参数。位置相关参数是顺序传入的,而关键词参数则可以乱序传入,比如:

这句代码会输出:

总之在调用的时候,参数顺序应该满足的规则是:

● 位置相关参数不能在关键词参数之后。

● 位置相关参数优先。

这么看有些抽象,不如看看两个错误用法,第一个错误用法:

这句代码会报错:

报错的意思是位置相关参数不能在关键词参数之后。也就是说,必须先传入位置相关参数,再传入关键词参数。

再看第二个错误用法:

这句代码会报错:

报错的意思是函数的参数arg1接收到了多个值。也就是说,位置相关参数会优先传入,如果再指定相应的参数,那么就会发生错误。

3.4.8 修改传入的参数

先补充有关传入参数的两个重要概念:

● 按值传递:复制传入的变量,传入函数的参数是一个和原对象无关的副本。

● 按引用传递:直接传入原变量的一个引用,修改参数就是修改原对象。

在有些编程语言中,可能是两种传参方式同时存在、可供选择,但是Python只有一种传参方式就是按引用传递,比如:

注意在函数内通过append修改了mylist的元素,由于mylist是list1的一个引用,因此实际上修改的就是list1的元素,所以这段代码会输出:

这是符合我们的预期的,但是看另一个例子:

按照之前的理论,number应该是num的一个引用,所以这里应该输出3,但是实际上的输出是:

为什么会这样呢?在第6章提到:特别地,字符串是一个不可变的对象。实际上,包括字符串在内,数值类型和Tuple也是不可变的,而这里正是因为num是不可变类型,所以函数的行为不符合我们的预期。

为了深入探究其原因,我们引入一个新的内建函数id,它的作用是返回对象的id。对象的id是唯一的,但是可能有多个变量引用同一个对象,比如下面这个例子:

我们可以得到这样的输出(这里id的输出不一定跟本书一致,但是第1,2,4个id应该是相同的):

其实除了函数参数是引用传递,Python变量的本质就是引用。这也就意味着在把alice赋值给bob的时候,实际上是把alice的引用给了bob,于是这时候alice和bob实际上引用了同一个对象,因此id相同。

接下来修改了alice的值,可以看到Bob的值并没有改变,这符合我们的直觉。但是从引用上看,实际发生的操作是,bob的引用不变,但是alice获得了一个新对象的引用,这个过程充分体现了数值类型不可变的性质——已经创建的对象不会修改,任何修改都是新建一个对象来实现的。

实际上,对于这些不可变类型,每次修改都会创建一个新的对象,然后修改引用为新的对象。在这里,alice和bob已经引用两个完全不同的对象了,这两个对象占用的空间是完全不同的。

那么回到最开始的问题,为什么这些不可变对象在函数内的修改不能体现在函数外呢?虽然函数参数的确引用了原对象,但是我们在修改的时候实际上是创建了一个新的对象,所以原对象不会被修改,这也就解释了刚才的现象。如果一定要修改的话,可以这么写:

这样输出就是我们预期的3了。

特殊地,这里举例用了一个很大的数字是有原因的。由于0~256这些整数使用地比较频繁,为了避免小对象的反复内存分配和释放造成内存碎片,所以Python对0~256这些数字建立了一个对象池。

我们可以得到输出(这里输出的两个id应该是一致的,但是数字不一定跟本书中的相同)为:

可以看出,虽然alice和bob无关,但是它们引用的是同一个对象,所以为了方便说明之前取了一个比较大的数字用于赋值。

3.4.9 函数的返回值

1.返回一个值

函数在执行的时候,会在执行到结束或者return语句的时候返回调用的位置。如果我们的函数需要返回一个值,那需要用return语句,比如最简单地返回一个值:

这段代码会输出:

这个multiply函数将输入的两个参数相乘,然后返回结果。

2.什么都不返回

如果我们不想返回任何内容,可以只写一个return,它会停止执行后面代码的立即返回,比如:

这里只要函数参数不是' secret '就不会输出任何内容,因为return后面的代码不会被执行。另外return跟return None是等价的,也就是说默认返回的是None。

3.返回多个值

和大部分编程语言不同,Python支持返回多个参数,比如:

这里要注意接收返回值的时候不能再像之前用一个变量,而是要用和返回值数目相同的变量接收,其中返回值赋值的顺序是从左到右的,跟直觉一致。

所以这个函数的作用就是把输入的三个变量顺序翻转一下。

3.4.10 函数的嵌套

我们可以在函数内定义函数,这对于简化函数内重复逻辑很有用,比如:

这段代码会输出:

需要注意的一点是,内部的函数只能在它所处的代码块中使用,在上面这个例子中,inner在outer外面是不可见的,这个概念叫作作用域。

1.作用域

作用域是一个很重要的概念,我们看一个例子:

这里函数func2中能正常输出x1的值吗?

答案是不能。为了解决这个问题,需要用到Python的变量名称查找顺序,即LEGB原则:

● L: Local(本地)是函数内的名字空间,包括局部变量和形参。

● E: Enclosing(封闭)外部嵌套函数的名字空间(闭包中常见)。

● G: Global(全局)全局函数定义所在模块的名字空间。

● B: Builtin(内建)内置模块的名字空间。

LEGB原则的含义是,Python会按照LEGB这个顺序去查找变量,一旦找到就拿来使用,否则就到更外面一层的作用域去查找,如果都找不到就报错。

可以通过一个例子来认识LEGB,比如:

其中要注意的是func3没有Enclosing作用域,至于闭包是什么会在后面的章节中介绍到,这里只要理解LEGB原则就可以了。

2.global和nonlocal

根据上述LEGB原则,我们在函数中是可以访问到全局变量的,比如:

但是LEGB规则仿佛出了点问题,因为会报错:

这并不是Python的问题,反而是Python的一个特点,也就是说Python会在阻止用户在不知情的情况下修改非局部变量,那么怎么访问非局部变量呢?

为了修改非局部变量,需要使用global和nonlocal关键字,其中nonlocal关键字是Python3中才有的新关键字,看一个例子:

也就是说global会使得相应的全局变量在当前作用域内可见,而nonlocal可以让闭包中非全局变量可见,所以这段代码会输出:

3.4.11 使用轮子

这里的“使用轮子”可不是现实中那种使用轮子,而是指直接使用别人写好并封装好的易于使用的库,进而极大地减少重复劳动,提高开发效率。

Python自带的标准库就是一堆鲁棒性强,接口易用,涉猎广泛的“轮子”,善于利用这些轮子可以极大地简化代码,这里简单介绍一些常用的库。

1.随机库

Python中的随机库用于生成随机数,比如:

它会输出一个随机的[1,5)范围内的整数。我们无需关心它的实现,只要知道这样可以生成随机数就可以了。

其中import关键字的作用是导入一个包,有关包和模块的内容后面章节会细讲,这里只讲基本使用方法。

用import导入的基本语法是:import包名,包提供的函数的用法是包名.函数名。当然不仅函数,包里面的常量和类都可以通过类似的方法调用,不过我们这里会用函数就够了。

此外如果不想写包名,也可以这样:

然后就可以直接调用randint而不用写前面的random了。

如果有很多函数要导入的话,我们还可以这么写:

这样random包里的一切就都包含进来了,可以不用random直接调用。不过不太推荐这样写,因为不知道包内都有什么,容易造成名字的混乱。

特殊地,import random还有一种特殊写法:

它和import random没有本质区别,仅仅是给了random一个方便输入的别名rnd。

2.日期库

这个库可以用于计算日期和时间,比如:

这段代码会输出:

3.数学库

这个库有着常用的数学函数,比如:

这段代码会输出:

其中第二个结果其实就是0,但是限于浮点数的精度问题无法精确表示为0,所以我们在编写代码涉及浮点数比较的时候一定要这么写:

这里EPS就是指允许的误差范围。也就是说浮点数没有真正的相等,只是在一定误差范围内的相等。

4.操作系统库

这个库包含操作系统的一些操作,例如列出目录:

在之后的文件操作章节还会见到这个库。

5.第三方库

可以用第3章讲过的pip来方便地安装各种第三方库,比如:

通过一行指令我们就可以安装numpy这个库了,然后就可以在代码中正常import这个库:

这也正是pip作为包管理器强大的地方,方便易用。