程序员入伙书——函数初步

以前的几个章节里,大家一定常见到print(…)、range(…)、max(…)之类的结构,它们是什么呢?

它们叫函数。不用管这个名字听起来多么奇怪,和数学上的函数、方程的概念多么不同,先接受这个名字,后面我会说这个名字的来历。

比如说,我们在《程序在干什么?》一节的最后一段举过一个求平方根的例子,后来在《初涉算法》一节讲解过它。就拿这个程序为例,如果现在我想求(根号2 + 根号3) x (根号5 – 根号7),该怎么写程序?

当然我们可以把这个例子复制粘贴四遍,第一遍求根号2,第二遍求根号3,第三遍求根号5,第四遍求根号七,然后用这四个中间结果算出最后的值。

这个思路能干活,可就是直觉太傻太啰嗦。四段程序只有喂给它们的数字的差别,实在是太浪费了。所以人们想,可以引入数学上的函数y = f(x)概念,定义好这个函数的公式(f)之后,把参数(x)喂给它,让同一段代码对参数进行运算,运算结果放在y里,这样岂不节约?

好主意!我们现在把求平方根的算法定义成一个函数:

>>> def mysqrt(n):
    high = max(1, n)
    low = 0
    while True:
        result = (high + low) / 2
        if abs(result * result - n)  < n * 1E-8:
            break
        if result * result > n:
            high = result
        else:
            low = result
    return result

>>>

这段程序里最关键的是第一行,以关键字def引导的函数名mysqrt,其后以括号包围要传递给这个函数的参数n,最后是冒号表示:以下的缩进内容是用来定义这个函数的算法。

最后一行return result也很重要,见到return语句时,程序就会返回到使用函数的那个位置,在相应的mysqrt(x)的地方,放上本次运行得到的result值。这个值被称为返回值

说得再多不如实际做一次:

>>> (mysqrt(2) + mysqrt(3)) * (mysqrt(5) - mysqrt(7))
-1.2889720732435594
>>>

瞧,结果出来了!在这个四则运算里,mysqrt函数的那段程序总共被运行了四次,每进入这段程序一次,n都被代入一个不同的值:2、3、5、7。每次运行到return result一句时,就返回这个四则运算,直到所有的mysqrt(x)都拿到自己的返回值为止。程序使用函数的过程,叫做调用

一些老一代的编程语言对返回值比较看重,有返回值的才有资格叫“函数”(Function),没有返回值的被称为Subroutine(子例程)或者Procedure(过程),FORTRAN语言和PASCAL语言就是这样的,这两种语言里,对函数和子例程/过程的定义,使用的语法是不同的。就我个人见过的语言里,第一个统一了这两种概念的是C语言,C语言认为不返回值的例程只是函数的一种特例,因此把它们统称为函数。在英语世界里,这个统一过程顺理成章,十分自然,因为Function这个词可以译为“功能”,不管是返回值的还是不返回值的,叫功能总是没错的。而汉语世界的读者就很难接受不返回值的“函数”的概念。没事,多看看就习惯了。

函数可以有很多参数,就是说,括号里可以放很多值。例如Python语言的自带函数max()、min()、print()都可以接受多个参数。max(3, 12, 4, 5),顾名思义,返回所有参数最大的那个,12。而min(3, 12, 4, 5)则相反,返回最小的3。这两个函数都接受至少两个参数——求单个数字的最大值或最小值是没有意义的。求绝对值的函数abs()则接受正好一个参数,多也不行少也不行,理由也很明显。

函数也可以没有参数。例如:

>>> string = input()
A quick brown fox jumps over the lazy dog.
>>> string
'A quick brown fox jumps over the lazy dog.'
>>>

这个例子里,Python自带的input()函数读取你所做的键盘单行输入,把这个输入的行放到返回值里面,然后你就可以拿这个返回值做后期处理了。

来写个判断一个数字是否为质数(素数)的函数:

>>> def isprime(n):
    if not isinstance(n, int) or n < 2:    # 如果n不是整数,或小于2
        return False                       # 返回“假”
    if n == 2:                             # 如果n等于2
        return True                        # 是质数,返回“真”
    for i in range(2, int(n ** 0.5) + 2):  # i从2循环到根号n,检测:
        if n % i == 0:                     # 如果n能被i整除
            return False                   # 则不是质数,返回“假”
    return True                            # 被以上循环筛选过的是质数
>>>

我已经把注释写在每行代码后头,希望大家能看懂。然后咱们做几个实验:

>>> isprime(1)
False
>>> isprime(2)
True
>>> isprime(3)
True
>>> isprime(4)
False
>>> isprime(5)
True
>>> isprime(6)
False
>>> isprime(7)
True
>>> isprime(8)
False
>>> isprime(9)
False
>>> isprime(10)
False
>>> isprime(11)
True
>>>

结果还挺对的,是吧?我们再狂野一些,拿这个函数验证一下哥德巴赫猜想——任意大于2的偶数可以表示为两个质数的和。注意,我们是要验证,不是证明。这个验证,是指定一个区间,让程序把这个区间里的偶数逐个拆分成两个质数,从而局部验证这个猜想。因为整数没有上限,所以我们无法真正做到穷举所有整数——假如我们能做到,这个猜想就算是被证明了,而不是现在这个半死不活的样子。

验证的思路很简单,对于任何一个偶数n,我们使用变量i做从2到n/2的循环,当我们发现i和n-i同时为质数时,对于n来说,哥德巴赫猜想就算是成立的。

>>> def goldbach(n):
    if n % 2 != 0 or n < 4:                  # 如果n不是偶数,或小于4
        return None                          # 既不算证明也不算推翻
    for i in range(2, n // 2 + 1):           # i从2循环到n的一半
        if isprime(i) and isprime(n - i):    # 如果i和n-i都是质数
            print(n, ' = ', i, ' + ', n - i) # 打印加法算式
            return True                      # 猜想对整数n成立
    return False   # 否则猜想被此整数推翻,事实上到目前为止,这句话从未被执行

>>>

好了,我们已经写好了两个函数:isprime(n)检查数字n是不是质数,goldbach(n)通过调用isprime(),来检查哥德巴赫猜想对数字n是否成立。现在我们就来实际跑一遍:

>>> for n in range(4, 52, 2):     # 从4循环到50,每次增量为2,循环:
    if goldbach(n) == False:      # 如果任何一次测试返回False,则此猜想被推翻。
        print("Goldbach's Conjectur is False on ", n)
        break

4  =  2  +  2
6  =  3  +  3
8  =  3  +  5
10  =  3  +  7
12  =  5  +  7
14  =  3  +  11
16  =  3  +  13
18  =  5  +  13
20  =  3  +  17
22  =  3  +  19
24  =  5  +  19
26  =  3  +  23
28  =  5  +  23
30  =  7  +  23
32  =  3  +  29
34  =  3  +  31
36  =  5  +  31
38  =  7  +  31
40  =  3  +  37
42  =  5  +  37
44  =  3  +  41
46  =  3  +  43
48  =  5  +  43
50  =  3  +  47
>>>

这个实验证明,对于50以内的全部偶数,猜想都是成立的。你可以试着把验证区间调得大些,但我很有把握地告诉大家,在计算机可以表达的数值范围内,那个print函数从来都执行不到。

思考题:

  • 为什么isprime()函数里,i循环到根号n就可以了?为什么不一直循环到n?
  • 为什么goldbach()函数里,i循环到n/2就可以了?为什么不一直循环到n?
评论关闭