Python自学之路六:函数

到目前为止,我们的Python代码已经实现了小的分块。它们都适合处理微小任务,但是我们想复用这些代码,所以需要把大型代码组织成可管理的代码段。代码复用的第一步是使用函数,它是命名的用于区分的代码段。函数可以接受任何数字或者其他类型的输入作为参数,并且返回数字或者其他类型的结果。

定义与调用函数

为了定义Python函数,你可以依次输入def、函数名、带有函数参数的圆括号,最后紧跟一个冒号(:)。函数命名规范和变量命名一样,必须使用字母或者下划线_开头,仅能含有字母、数字和下划线。

我们先定义和调用一个没有参数的函数,这个是最简单的Python函数:

定义函数的下面一行需要像声明if语句一样缩进,Python函数中的pass表明函数没有做任何事情。接下来通过输入函数名和参数调用此函数,像前面说的一样,它没有做如何事情:

现在定义一个无参数,但打印输出一个单词的函数,当调用are_you_ok()函数时,Python会执行函数内部的代码:

下面用return关键字指定函数返回的值:

学到这里已经迈出了很大的一步,在函数中,使用if判断和for/while循环组合能实现之前无法实现的功能。是时候在函数中引入参数了,定义带有一个name参数的函数student_name(),它使用return语句将alist返回给它的调用者两次并在中间加入一个逗号,然后用字符串'Kim'调用函数student_name():

传入到函数的值称为参数,当调用含参数的函数时,这些参数的值会被复制给函数中的对应参数。在上面这个例子中,被调用的函数student_name()的传入参数字符串是'Kim',这个值被复制给参数name,然后返回调用方。

这个函数在print之前做了以下事情:

  • 把'my_list'赋值给函数内部参数alist
  • 运行if-elif-else的逻辑链
  • 返回一个字符串
  • 将该字符赋值给变量my_sum

一个函数可以接受任何数量(包括0)的任何类型的值作为输入变量,并且返回任何数量(包括0)的任何类型的结果。如果函数不显示调用return函数,那么会默认返回None

有用的None

None是Python中一个特殊的值,虽然它不表示任何数据,但仍然具有重要的作用。虽然None作为布尔值和False是一样的,但是它和False有很多差别。看下面这个例子:

虽然这是一个很微妙的区别,但是对于Python来说是很重要的。你需要把None和不含任何值的空数据结构区分开来。0值的整数型/浮点型、空字符串、空列表、空元组、空字典、空集合都等价于False,但不等于None。例如下面这个,写一个函数,输出它的参数是否是None,然后运行一些测试函数:

函数的参数

  • 形式参数:函数创建和定义过程中,函数名后面括号里的函数,它只是代表一个位置、一个变量名。
  • 实际参数:函数在调用过程中传入的参数,是一个具体内容,赋值变量的值。
  • 位置参数:调用函数的时候传入参数的值是按照顺序复制过去的,不带有key,但是弊端时必须熟记每个位置的参数的含义,不能把参数放错位置。
  • 关键字参数:调用函数时指定对应参数的名字,甚至可以采用与函数定义不同的顺序调用。
  • 混用位置参数和位置参数:如果混用,所有位置参数必须在前,关键字参数必须在后。

指定默认参数值

当调用方没有提供对应的参数值时,你可以指定默认参数值,这个听起来很普通的特性实际上特别有用。默认参数值在函数被定义时已经计算出来,而不是在程序运行时,所以不能把可变的数据类型当作默认参数值

在下面的例子中,函数alist()在每次调用时,添加参数arg到一个空的列表result,然后打印输出一个单值列表。但是存在一个问题:只有在第一次调用时列表是空的,第二次调用时就会存在之前调用的返回值:

如果写成下面的样子就会解决刚才的问题:

使用*收集位置参数

如果在之前有使用过C/C++,可能会认为Python中的星号(*)和指针相关,然而,Python是没有指针概念的。当参数被用在函数内部时,星号将一组可变数量的位置参数集合成参数值的元组。在下面的例子中args是传入到函数print_args()的参数值的元组:

当使用*时,不一定要把元组参数命名为args,但这是Python中的一个常见做法

使用**收集关键字参数

用两个星号可以将参数收集到一个字典中,参数的名字是字典的键,对应参数的值是字典的值。如果把带有*args**kwargs的位置参数混合起来,它们必须按照顺序出现。下面这个例子定义了函数print_kwargs(),然后打印输出它的关键字参数:

关键字参数命名和args一样

内部函数和匿名函数

在Python中,可以在函数中定义另外一个函数。当需要在函数内部多次执行复杂的任务时,内部函数是非常有用的,从而避免了循环和代码的堆叠重复。例如:

闭包:内部函数可以看作一个闭包。闭包是一个可以由另一个函数动态生成的函数,并且可以改变和存储函数外创建的变量的值。在上面这个例子中,inner()函数可以得到a,b参数的值并且记录下来,return inner这一行返回的是inner函数的复制(没有直接调用),所以它就是一个闭包:一个被动态创建的可以记录外部变量的函数。

Python 中,lambda函数是用一个语句表达的匿名函数。可以用它来代替小的函数。通常,使用实际的函数会比使用lambda更清晰明了。但是,当需要定义很多小的函数以及记住它们的名字时,lambda会非常有用。尤其是在图形用户界面中,可以使用lambda来定义回调函数。比如下面这个例子:

生成器和装饰器

生成器

生成器是用来创建Python序列的一个对象。使用它可以迭代庞大的序列,且不需要在内存中创建和存储整个序列。通常,生成器是为迭代器产生数据的。在之前我们讲过range()来产生一系列整数。每次迭代生成器时,它会记录上一次调用的位置,并且返回下一个值。这一点和普通的函数是不一样的,一般函数都不记录前一次调用,而且都会在函数的第一行开始执行。

如果你想创建一个比较大的序列,使用生成器推导的代码会很长,这时可以尝试写一个生成器函数。生成器函数和普通函数类型,但是它的返回值使用yield语句声明而不是return。下面我们编写我们自己的range()函数版本:

装饰器

有时你需要在不改变源代码的情况下修改已经存在的函数,常见的例子是增加一句调试声明,以查看传入的参数。装饰器实质上是一个函数,它把一个函数作为输入并且返回另外一个函数。在装饰器中,通常使用下面这些Python技巧:

  • *args和**kwargs
  • 闭包
  • 作为参数的函数

看下面的代码:

无论传入document_it()的函数func是什么,装饰器都会返回一个新的函数,其中包含函数document_it()增加的额外语句。实际上,装饰器并不需要执行函数func中的代码,只是在结束前函数document_it()调用函数func以便得到func的返回结果和附加代码的结果。
那么,如何使用装饰器?当然,可以通过人工赋值:

作为对前面人工装饰器赋值的替代,可以直接在要装饰的函数前添加装饰器名字@decorator_name:

同样一个函数可以有多个装饰器。下面,我们写一个对结果求平方的装饰器square_it()

靠近函数定义(def 上面)的装饰器最先执行,然后依次执行上面的,任何顺序都会得到相同的最终结果。

命名空间和作用域

一个名称在不同的使用情况下可能指代不同的事物。Python程序有各种各样的命名空间,它指的是在该程序段内一个特定的名称是独一无二的,它和其他同名的命名空间是无关的。每一个函数定义自己的命名空间。如果在主程序(main)中定义一个变量x,在另外一个函数中也定义x变量,两者指代的是不同的变量。但是,天下也没有完全绝对的事情,需要的话,可以通过多种方式获取其他命名空间的名称。每个程序的主要部分定义了全局命名空间。因此,在这个命名空间的变量是全局变量。

你可以在一个函数内得到某个全局变量的值:

为了读取全局变量而不是函数中的局部变量,需要在变量前面显式地加关键字global,如果在函数中不声明关键字global,Python会使用局部命名空间,同时变量也是局部的,函数执行后回到原来的命名空间:

Python还提供了两个获取命名空间内容的函数:locals()返回一个局部命名空间内容的字典,globals()返回一个全局命名空间内容的字典。

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注