自学Python:编程基础、科学计算及数据分析
上QQ阅读APP看书,第一时间看更新

2.4 函数与模块

函数和模块是Python可扩展性的一个重要组成部分。函数可以帮助我们复用代码,减少代码量;模块可以让我们使用已有的现成代码,避免重复工作。

2.4.1 函数

在计算机科学中,函数(Function)是程序负责完成某项特定任务的代码单元,具有一定的独立性。函数通常通过名称进行调用,调用时用一个小括号接受参数,并得到返回值,例如:

c = max(a, b)

1. 函数的定义

在Python中,函数在定义时需要满足这样的规则:

● 使用关键字def引导;

● def后面是函数的名称,括号中是函数的参数,不同的参数用逗号“,”隔开,参数可以为空,但括号不能省略;

● 函数的代码块要缩进;

● 用一对"""包含的字符串作为函数的说明,用来解释函数的用途,可省略,在查看函数帮助时会显示;

● 使用关键字return返回一个特定的值,如果省略,返回None。

例如,我们这样定义一个加法函数,它接受两个变量x和y,计算x与y的和a,并返回a的值:

2. 函数的调用

定义好函数后,函数并不会自动执行。我们需要调用它才能执行相关的内容。

函数的调用使用函数名加括号的形式,参数放在括号中:

Python不限定函数参数的输入类型,因此,我们可以使用不同类型的对象作为参数,只要这些传入的参数支持相关的操作。

对于add()函数,由于数字和字符串都支持加法操作,因此上面的调用都是合法的。

当传入的两个参数不支持加法时,Python会将抛出异常:

当传入的参数数目与实际不符时,也会抛出异常:

传入参数时,Python提供了两种模式,第一种是按照参数的顺序传入,另一种是使用键-值模式,按照参数名称传入参数。

例如,使用键-值模式,上面的例子可以写成:

两种模式可以混用:

In [9]: add(2, y=3)

Out[9]: 5

在混合使用时,需要注意不能给同一个值赋值多次,类似于add(2, x=3)的形式会抛出异常。

3. 带默认参数的函数

我们可以给函数参数设定默认值,默认参数需要在定义设定。

例如,定义一个一元二次函数,并让参数a、b、c带有默认值:

定义时,所有带默认值的参数必须放在不带默认值的参数后面。

只传入x,其他的都用默认参数:

In [11]: quad(2.0)

Out[11]: 4.0

传入x和b:

In [12]: quad(2.0, b=3)

Out[12]: 10.0

传入x、a和c,参数b用默认:

In [13]: quad(2.0, 2, c=4)

Out[13]: 12.0

4. 接受不定数目参数的函数

有些函数可以接受不定数目的参数,如max()和min()等。不定数目参数的功能,可以在定义函数时使用星号“*”来实现。

例如,我们修改add()函数,使其能实现两个或多个值的相加:

参数中的*ys是一个可变数目的参数,我们可以把它看成一个元组。

调用add(1, 2, 3, 4)时,第一个参数1传给了x,剩下的参数组成一个元组传给了ys,因此,ys的值为(2, 3, 4)。

我们还可以使用任意键值作为参数,这种功能可以在函数定义时使用两个星号“**”实现:

**ys表示这是一个不定名字的参数,它本质上是一个字典。

调用add(1, y=2, z=3, w=4)时,ys为字典{'y': 2, 'z': 3, 'w': 4}。

这两种模式可以混用,不过要按顺序传入参数,先传入位置参数,后传入关键字参数:

反过来,我们可以在元组或者字典前加星号,将其作为参数传递给函数,不过这样的用法不是特别常见:

5. 返回多个值的函数

函数可以返回多个值。例如,下面的函数返回一个序列的最大值和最小值:

事实上,Python返回的是一个元组,只不过元组的括号被省略了。对于返回的元组,我们使用了Python的多变量赋值机制将它赋给了两个值:

In [30]: min_max(t)

Out[30]: (1, 9)

2.4.2 模块

1. 模块简介

退出IPython解释器重新启动时,之前定义的函数和变量都会丢失。如果要写一个稍微长一点的程序,使用解释器就不是那么方便,这就需要使用脚本模式进行处理。

在使用脚本模式时,随着代码量的增多,我们可能需要将一个文件切分成多个,以便管理和维护。在多个文件中使用一些公共的函数和变量时,我们不希望在每个文件里都复制粘贴一份定义。Python提供了模块机制来完成这种功能。

使用模块机制需要将公用的函数和变量放到一个文件中,然后从别的脚本或者解释器模式中导入这个文件中的内容。这个包含Python函数和定义的文件就是一个模块(Module)。通过模块,我们可以复用现成的代码,减少工作量。

在Python中,所有以.py结尾的文件都可以被当作一个模块使用。

例如,有这样一个文件ex1.py:

在Ipython解释器中,可以使用魔术命令%%writefile来创建这个文件:

如果直接使用魔术命令%run来运行这个脚本,会输出结果:

In [2]: %run ex1.py

6 3.1416

这个脚本可以被当作一个模块,导入到解释器或者其他脚本中。

在Python中,导入模块使用关键字import:

import <module>

<module>是不带.py后缀的文件名,即:

In [3]: import ex1

6 3.1416

ex1是一个被导入的模块:

In [4]: ex1

<module 'ex1' from 'ex1.py'>

在导入时,Python会执行一遍模块中的所有内容,所以ex1.py文件中print的结果被输出了。

导入模块之后,ex1.py中的变量都被载入了当前命名环境中,不过,我们需要使用“<module>.<variable>”的形式来调用它们:

In [5]: ex1.PI, ex1.w

Out[5]: (3.1416, [0, 1, 2, 3])

变量可以被修改:

In [6]: ex1.PI = 3.14

可以用“<module>.<function>”的形式调用模块里面的函数:

In [7]: ex1.my_sum([2, 3, 4])

Out[7]: 9

为了提高效率,在同一个程序中,已经载入的模块再次载入时,Python并不会真正执行载入操作,即使模块的内容已经改变。

例如,再次导入模块时,程序ex1.py的结果不会再次输出:

In [8]: import ex1

在导入模块时,Python会对ex1.py文件进行一次编译,得到以.pyc结尾的文件ex1.pyc,这是Python生成的二进制程序表示。

2. __name__变量

我们在导入模块时,有时候不希望执行脚本中的某些语句,如ex1.py中的print语句等,这种效果可以借由__name__变量来实现。

__name__变量在Python文件作为脚本执行或者作为模块导入时的值有一定的差异。作为脚本执行时,变量__name__对应的值是字符串'__main__';而作为模块导入时,变量__name__对应的值是模块的名称。因此,可以在ex1.py中的print语句前面加上对__name__变量的判断语句,得到文件ex2.py:

作为脚本运行它时,条件满足:

In [9]: %run ex2.py

6 3.1416

作为模块导入它时,条件不满足,判断里面的内容不会被执行:

In [10]: import ex2

3. 模块的导入

除了import关键字直接导入模块的形式,模块还有一些其他的导入方式,我们可以根据实际需要选择合适的方式导入模块。

模块的导入方式如表2-11所示。

表2-11 模块导入方式

导入模块时,Python会优先在当前程序的工作目录中寻找模块;如果找不到,则会去Python系统的工作目录中寻找。路径的查找顺序可以用sys模块的变量sys.path查看。

默认情况下,模块会导入脚本中所有已定义的内容,这样做有可能导入了一些不需要的内容。为此,我们可以通过设置__all__变量来限制导入的内容。在Python中,__all__变量可以用来控制模块导入的内容。

例如,在ex1中,假设我们只需要导入PI、my_sum,不需要w,可以在ex1.py中,令:

__all__ = [PI, my_sum]

这样,导入ex1时,我们就只会导入PI和my_sum两个部分了。

4. dir()函数

我们可以用dir()函数查看一个模块中所包含的所有对象:

In [11]: dir(ex2)

Out[11]: ['__name__', 'my_sum', 'PI']

如果不给定参数,dir()函数返回当前已定义的所有变量。

5. 包

(Package)是一个由多个模块组成的集合,用来管理多个模块。

一个Python包的基本结构如下:

foo是一个文件夹,其他的是文件。其中,__init__.py文件必不可少,它可以是一个空文件。可以通过调用import foo来导入__init__.py文件中的内容。

包内的两个子模块bar.py、baz.py可以通过这样的方式来调用:

使用from <package> import <item>的形式时,<item>既可以是一个子模块,也可以是模块中的一个函数或变量。

除了子模块,包内还可以含有子包。例如,foo是一个含有子包的Python包:

子包中的模块可以相互调用。假设我们在foo/format文件夹中的bar.py文件中,希望导入foo包中其他的子模块或子包,可以使用以下方式:

其中,“.”代表当前文件夹,“..”代表当前文件夹的父文件夹。

在后文中,为了方便叙述,我们将模块统称为模块