Python函数式编程01 函数的基本概念

什么是函数

函数(function)是可以重复使用的代码片段。在不同的编程语言里,它可能有不同的称呼,例如子程序、方法、过程等。但它们的本质都是类似的。

当经常性地遇到一种或一类的问题时,需要多次使用相同或类似的代码。这个时候,就可以将这一段代码编写成函数,利用函数来执行一种或一类功能,从而更加简洁、逻辑清晰。

例如,考虑有以下列表:

ls = [2, 4, 6, 7, 4]

假设需要计算列表内所有元素的和,这个时候,一般来说可以编写以下代码:

total = 0
for i in ls:
    total += i

这当然没有问题。但是,如果每次遇到计算元素的和,都需要编写这样一段代码,不仅比较麻烦,而且还容易发生疏忽,编写完的程序看上去都是一些简单的命令,无法很快明白其具体用途。

这个时候,一个更好的思路是将其抽象为一个过程,每次只需要表示需要处理一段类似的过程即可,至于具体的处理细节可以预先定义好。

函数是具有一定功能的一些代码组成的一个抽象整体,在编写程序时往往只需要关注整体将实现哪些功能,而不需要明白具体的实现步骤如何。编程语言上的函数实质上很类似于数学上的函数。

如果使用 Python 的内置函数(built-in functions),以上代码可以代替为:

total = sum(ls)

sum() 函数将一个处理逻辑抽象为了求和过程,无论是编程程序还是阅读程序,都可以很好地明白该代码的工作内容和结果。

函数将代码分割成不同的区域,每个区域只需实现一个或一类特定的功能,让代码更模块化,在阅读代码时,如果遇到一个函数,很容易就可以明白它的功能。至于具体的实现方法,只需找到函数的位置就可以明白。因此即便有些时候,一个功能只需使用一次,也可以编写一个函数来处理这个功能。

Python中的函数

函数与参数

首先来看看如何定义函数。函数的定义函数的定义需要使用关键字 def ,空格后紧跟一个函数名,函数名后面有一对圆括号,接着以一个冒号收尾:

def function_name():
    ...  # function body

这样就完成了一个函数的定义了。在需要的时候,便可以使用函数,来执行函数体内的代码。

关于定义函数,需要注意以下几点:

  • 函数使用关键字 def 声明,函数名为有效的标识符,Python 的一项提议建议函数名统一采取小写的蛇形命名法,用下划线连接不同单词
  • def 关键字引导的是复合语句,冒号后面的函数体内部需要采取缩进的形式表示这些语句属于该函数的部分
  • 一个函数创建后,它不会立即执行,只有需要时才会执行。函数名用于标识一个函数对象,可以通过一个函数名执行函数内包含的代码
  • 正因如此,一般在编写程序时,都会将函数的定义置于主体程序的上方,统一管理

为了能让函数处理过程相似、但被处理对象完全不同的逻辑,一个函数需要获取外部的变量,并根据获取的变量决定需要得到什么样的结果。考虑以上对列表求和的代码片段,如果要对不同的列表求和,往往会得到不同的结果。

一个函数可以具有一个、多个或没有参数(argument),参数用于接收外部数据,并根据数据的不同执行不同的处理方式。

为了向函数传递参数,函数在设计时就必须具有容纳并处理参数的功能。具体方式是在函数定义的小括号内,使用一个或多个标识符来表示这里有参数:

def function_name(param1, param2, ..., param3):
    ...  # function body

由于函数在定义时,还不知道具体传入的是哪个参数,因此这里先用一个标识符代表具体的参数,在函数体内处理这个标识符,就好像处理实际传入的值一样,因此函数定义中表示参数的标识符也称为形式参数,即虽然它不是一个外界传入的具体值,但在函数体内还是将其作为一个外界传入的具体值对待。

例如,以下函数定义:

def average(seq):
    total = length = 0
    for i in seq:
        total += i
        length += 1
    print('average: ', total / length)

将标识符 seq 作为外界传入的参数对待,并对它调用 for 循环。在调用时,实际得到的参数就会替代 seq ,作为真正被 for 循环遍历的对象。

函数的调用与返回值

知道了如何创建函数,接下来是关于函数的调用。前面说过在使用 def 关键字创建函数后,其函数名就是一个变量,它绑定了函数体。因此,调用函数的方式为在函数名后面跟上一对圆括号,即可完成对函数的调用:

function_name()

这样,程序运行到该处,就会执行函数体内对应的语句。

如果函数有参数,那么在调用时,需要将参数依次填入括号中,如下:

function_name(arg1, arg2, ..., arg3)

注意参数的位置需要和定义时一致。下图展示了函数调用时,参数是如何传入与起作用的:

在调用时传入的参数也称为实际参数。形式参数说明参数是如何参与运算的,实际参数用于计算运算的最终结果。


既然外界可以通过参数向函数传递信息,那么函数在完成计算后,应该也可以将结果返回给外界。否则外界便无法利用其计算结果。Python 中,一个函数具有返回值,返回值用于将一个结果传递给外界,让外界收集并利用函数计算的结果。

返回语句使用关键字 return 开头,后面跟随一个表达式。函数执行到 return 语句后,会立即退出函数的执行流畅,并将后面的表达式作为函数的运算结果:

def function_name(param1, param2, ..., param3):
    ...  # function body
   return expression

下面实现了这样一个 get_max() 函数,用于计算一个序列中元素的最大值。当遍历完序列,得到最大值后,它并没有将结果打印出来,而是作为返回值传递给外界:

def get_max(seq):
    if not seq:
        return
    max_value = seq[0]
    for i in seq:
        if i > max_value:
            max_value = i
    return max_value

如果一个函数不带 return 语句或 return 语句后面不带表达式,它会返回空值 None 。以上函数在判断传入的序列不可循环后,使用空的 return 语句强制退出函数。

可以使用赋值语句将结果保存到一个变量内:

result = get_max([2, 4, 6, 7, 4])

因此,可以认为一个函数的调用就是将一个函数替换为返回的表达式,下图表达了这种思想:

在明白了函数、参数和返回值后,可以完全将其类比到数学概念上的函数内。假设有以下函数:

\\[ f(x, y) = e^y + \log _2 (x) \dot \sin (x) \\]

那么它的函数名为 \\( f \\) ,具有两个参数 \\( x \\)\\( y \\) ,尽管它们都不是具体的值,但是从函数的结构上,还是可以推断出该函数的定义域、值域等内容。

一旦给定了自变量的具体值,就可以计算出函数的具体结果。例如,给定参数的具体值 \\( f(\frac \pi 2, 1) \\) ,该函数的计算结果为 \\( \log _2 \pi + e -1 \\) ,得到的就是函数的返回值。

变量的作用域

在介绍返回值前,或许有人会想过可以直接将结果保存在变量内,然后在函数外部引用保存的变量来获取返回结果,例如以下代码:

def length(seq):
    n = 0
    for i in seq:
        n += 1

print(n)

但是,这样是错误的做法,尝试运行的结果会发生错误,错误原因是变量未被定义:

>>> def length(seq): ... n = 0 ... for i in seq: ... n += 1 ... >>> print(n) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'n' is not defined

虽然函数在调用时,函数体内的代码会被执行,就像将其放在函数调用的位置一样,但是在函数体内定义的变量,有一个变量的作用域的概念。简单地说,在函数内定义的变量,在函数执行完成后,会销毁所有定义的变量,释放这部分被占用的内存。

因此,以上函数实际上的执行过程类似:

def length(seq):
    n = 0
    for i in seq:
        n += 1
    del n
    return

在函数内重新定义的变量,也被称为局部变量,因为它们只在函数体内有效,函数体外无法访问,对函数体外的同名变量不产生任何影响。

实际上,函数的参数也可以看作是一个局部变量,它在参数传递的时候被创建并赋予对应的值,并在函数调用完成后被销毁。

以下给出了这样一个示例:

n = 3
print(n)      # n 为定义的值
def test01(n) :
    print(n)  # n 为使用的参数
    n = 10
    print(n)  # n 为重新定义的值
test01(5)
print(n)      # n 仍为函数体外定义的值

该函数的运行结果如下,注意观察变量的值是如何反映同名变量的创建与销毁的:

$ python -u demo.py 3 5 10 3

调用完函数后,外部变量的值仍然是 3 ,说明局部变量值的修改不会影响全局变量。

不过还存在一种情况,考虑以下冒泡排序函数:

def bubble_sort(seq):
    for i in range(1, len(seq)):
        for j in range(0, len(seq)-i):
            if seq[j] > seq[j+1]:
                seq[j], seq[j + 1] = seq[j + 1], seq[j]
    return seq

这是一个很简单的函数,但是如果尝试调用它,看看会发生什么:

$ python -u demo.py >>> data = [5, 1, 4, 3, 10, 4, 2, 7, 7, 9] >>> bubble_sort(data) [1, 2, 3, 4, 4, 5, 7, 7, 9, 10] >>> data [1, 2, 3, 4, 4, 5, 7, 7, 9, 10]

调用函数之后,再次检查列表 data 的值,可以发现该列表的值发生了变化。但是,在函数体内,自始至终只涉及到对参数的修改。

要解答这个问题,必须先了解 Python 中的可变对象与不可变对象的概念。

可变对象与不可变对象

Python 中的对象可以分为可变(mutable)对象与不可变(immutable)对象。不可变对象不能修改,只能重新赋值,这个过程实际上是新生成了一个对象,再让变量指向它,而旧对象被丢弃。而对可变对象的修改,则是将其中的一部分值更改,变量本身并没有修改。

注意以上的说法:对可变对象的修改,那么之前的排序函数哪里有对可变对象的修改呢?答案就在这条语句:

seq[j], seq[j + 1] = seq[j + 1], seq[j]

这条语句中,将列表的某个元素与其后一个元素交换位置,这一操作就发生了对列表的修改,但是列表作为一个整体还是之前的列表,并没有变成另一个新列表。

在 Python 中,数值、字符串和元组是不可更改的对象,对它们调用任何方法都只能得到一个新的数值、字符串和元组;而列表、集合和字典则是可以修改的对象,它们随时可以增加、改变或删除包含的某些元素。

在 Python 中,可以通过 id(obj) 函数来查看某一变量的唯一身份标识。它接收一个变量作为参数,返回的结果是一个数值,不同对象得到的数值不同。

以下测试函数用于检查修改可更改对象修改前后的 id 值:

m = [1]
print(id(m))
def modify_m(m):
    print(id(m))
    m.append(5)
    print(id(m))
modify_m(m)
print(id(m))

结果为:

$ python -u demo.py 1980965310984 1980965310984 1980965310984 1980965310984

结果表明,在函数体内外、修改变量前后,四个 id 值都相同,指向的是同一个对象。

global语句

修改可变对象是一种在函数内部影响函数外部变量的一种方式。在函数体内,对变量重新赋值会使变量指向另一个对象,要使函数体内修改一个变量,让其指向别的对象,并且作用于函数体外,需要将其标示为全局变量。

可以使用 global 语句将一个或一些变量标示为全局变量:

def function_name(param, ...):
    global var1, var2, ...

global 关键字会将跟随的变量表示为全局变量,使得影响范围不局限于函数体内部,对其任意修改、赋值均会对函数内外的变量值造成改变。

global 关键字可以位于函数的任意位置,只有一个条件:其声明为全局的变量不能在函数体的前面被赋值。否则,编译器无法判断是否需要让前面的赋值作用到全局范围内。由于函数的形式参数会在函数调用时被赋值为实际参数的值,因此不能对形式参数使用 global 声明。

以下示例检查一个 global 声明前后、函数内外变量的 id 值:

im = 1
print(id(im))
def modify_im():
    global im    
    print(id(im))
    im = 5
    print(id(im))
modify_im()
print(id(im))

结果为:

$ python -u demo.py 1474521856 1474521856 1474521984 1474521984

可以看到,在对 global 声明的变量赋值后,全局变量指向的对象也改变了,这说明函数体内修改的是全局变量而不是局部变量。

京ICP备2021034974号
contact me by hello@frozencandles.fun