4.求$ \sum_{i=1}^mi + \sum_{i=1}^ni + \sum_{i=1}^ki$

4.1 任务简介与分析
当然,这个任务仍然是不允许采用等差数列求和的公式来计算。任务没有什么难度,就是多次求和的过程。

4.2 软件复用

  • 键入如下代码并观察执行结果。
#求sigma1~m + sigma1~n + sigma1~k

n = int(input('请输入第1个整数,以回车结束。'))
i = 0
total_n = 0
while i < n:
    i = i + 1
    total_n = total_n + i

m = int(input('请输入第2个整数,以回车结束。'))
i = 0
total_m = 0
while i < m:
    i = i + 1
    total_m = total_m + i

k = int(input('请输入第3个整数,以回车结束。'))
i = 0
total_k = 0
while i < k:
    i = i + 1
    total_k = total_k + i

print('最终的和是:', total_n + total_m + total_k)

好,一次闯关成功,任务解决,so easy,赶紧进入下一个任务。但等等,先别急,逐行敲入代码或者是拷贝剪贴代码再修改的读者,是否觉得这样的工作是无聊的重复劳动?到本节为止,上段代码中间的6行,算上这三次,至少已经重复输入了四次,如果有其他任务或者实践需要类似的求和,则我们还将重复输入。
人们不喜欢一直简单重复,人们编程的初衷之一就是要利用计算机从简单重复中解救自己。如何能对类似的代码进行重复利用呢?既可以避免重复,又可以一次写好到处实用呢?这种不“重复制造轮子”的思想与方法称为软件复用

4.3 函数、参数与返回值

要是计算机看到compute_sum(1...end)compute_sum(1, end)或干脆就compute_sum(end)就能求得从1到n的和就好了。
compute_sum是说明要做什么,(end)是仿照python的print()及input()函数的形式,参数指明加到end为止。等等,这不就是函数吗?只不过这个函数是我们自己想的而已,如果可以,那上面的程序就可以简化成下面这样:

n = int(input('请输入第1个整数,以回车结束。'))
m = int(input('请输入第2个整数,以回车结束。'))
k = int(input('请输入第3个整数,以回车结束。'))

print('最终的和是:', compute_sum(m) + compute_sum(n) + compute_sum(k))

完美!看到这样的简化,不禁有些小激动了。这样非常清楚,非常符合逻辑。
computer_sum()虽然名称符合python命名规范与习惯,形式也与我们已经接触到的print()及input()函数相同,但这只是我们临时写的函数,函数是由我们自行定义的,一般称为自定义函数,不需要区分的时候,可简称函数。
到目前为止,python语言还不知道这个自定义函数是什么,因此无法解释执行它,那么,我们就教给python这个函数的定义,让python理解吧。但在真正尝试前,我们先从最简单的自定义函数开始。

  • 键入如下代码并观察执行结果。
# 第一个自定义函数
def fake_print(content):
    print(content)

一片空白,这不是你大脑的状态,因为大脑的状态也许是一堆问号---没有出错,但居然也没有其他任何反应(我一定写了假代码,笑)。
由于没有报错,毫无疑问,本段代码符合python语法,其中def是定义函数使用的关键字,也就是英文define的缩写;fake_print是作者刚刚起的函数名;括号内的content是函数参数。
函数定义的基本结构是:

def 函数名(参数表):
    语句块
  • 函数名(参数表),一般称为函数头
  • 函数定义内部的语句块又称为函数体
  • 参数表就是一系列参数,可以有0,1或多个参数,多个参数之间用,分隔。

函数定义好之后,只是能使python解释器知道有一个自定义函数,知道这个函数的名字,知道函数参数表,但是并不执行,也并不知道函数参数表内参数的具体值。 因此,函数定义时的参数,一般称为形式参数,简称形参
python解释器知道很多已经定义好的函数,比如input(),比如print(),但是如果我们不在代码中写出来,不去调用使用,函数就不会被执行。
这样,前面执行第一个自定义函数的代码,就只是让python解释器知道有一个自定义函数,名字是fake_print,有一个参数content,这些都属于函数头信息。
对自定义函数来说,仅让python解释器知道这个函数头的信息还不够,还需要在函数体实现函数的功能。这个功能是为以后函数被使用被调用时准备的。

  • 键入如下代码并观察执行结果。
# 函数定义
def fake_print(content):
    print(content)

# 主程序
fake_print('假装是print()函数。')

本段代码执行的大体流程是:

  • 对第一行代码def fake_print(content):进行处理,发现这是函数定义,于是保存此处地址,保存函数信息:函数名是fake_print,一个函数形参是content
  • 跳过fake_print()的函数体定义,直接从函数体外的第一行开始执行;
  • 在执行到主程序[^1]第一行fake_print('假装是print()函数。')时,发现这里调用了fake_print()函数,有一个实参与函数定义的形参content相对应,其值为'假装是print()函数。',于是将'假装是print()函数。'赋值给content;
  • 保存调用函数的地址,然后根据fake_print()函数的地址,跳转到def fake_print(content):,准备运行函数体。此时,形参content已有实际值,已经可以作为有实际值的变量在函数体内使用了。执行函数时,其形参值是由实际参数在函数调用时由对应的实参传递过来的,一般把这个参数传递过程称为参数传递。注意参数传递时,实参会赋值给相对应的形参,最一般的就是位置对应,因此,不需要形参与对应实参的变量名相同;
  • 运行函数体,调用print()函数打印'假装是print()函数。'
  • 函数体执行完毕,程序返回到调用fake_print()函数地址的行末,继续执行;
  • 执行到程序尾,整个程序执行完毕。

由自定义函数的执行过程可知:如果自定义函数在当前文件中定义,则函数定义的代码必须写在函数调用之前。有些“神秘”函数比如print()及input(),则不受此限制。实际上print()及input()等函数由于太过常用,函数已经被预先定义及实现好,并内置在Python语言中,称为内置函数
作为示例,fake_print()函数只完成了一个简单的功能,即与只带一个参数的print()函数功能相同。
回到求和问题中来,现在可以定义我们梦想中的compute_sum()函数了。

  • 键入如下代码并观察执行结果。
#函数定义
def compute_sum(end):
    i = 0
    total_n = 0
    while i < end:
        i = i + 1
        total_n = total_n + i

# 主程序
compute_sum(100)

很不幸,一片空白给兴致勃勃的我们当头一棒。但我们已经进行了函数定义,已经进行了函数调用,已经没有任何python语法错误提示了啊?
别急,让我们一起简单的分析下程序执行过程:

  • 第一行,python解释器保存函数头信息,保存调用地址,跳转到函数体;
  • 执行compute_sum(100)调用函数,将100传递给n;
  • 进入compute_sum函数体,从i=0开始执行,执行完毕时,应该能够得到total_n的值为5050;
  • 函数体结束,程序跳转到函数调用处末尾,即compute_sum(100))后面;
  • 函数结束。

好吧,我们没有把compute_sum()函数中计算的结果打印出来,了解了。

  • 键入如下代码并观察执行结果。
# 函数定义
def compute_sum(end):
    i = 0
    total_n = 0
    while i < end:
        i = i + 1
        total_n = total_n + i
        print(total_n)

# 主程序
compute_sum(100)

很奇妙,打印出了一大波数字,最后一个确实是5050,我们犯了一个常犯错误(请自行思考,python中空格缩进太重要了)。

  • 键入如下代码并观察执行结果。
# 函数定义
def compute_sum(end):
    i = 0
    total_n = 0
    while i < end:
        i = i + 1
        total_n = total_n + i
    print(total_n)

# 主程序       
compute_sum(100)

必须承认这次得到了预期的计算结果,5050。
可以预想到,每次调用compute_sum()函数时,随着参数的不同,可以计算出任意从1累加到end的值。但是先等等,我们怎么来完成我们最初的任务呢?
compute_sum()函数虽然能计算任意从1累加到end的值,但却无法将这些值累加。其实在compute_sum()函数中打印total_n并无必要,中间结果无需打印。
那么,这样试试:

  • 键入如下代码并观察执行结果。
# 函数定义
def compute_sum(end):
    i = 0
    total_n = 0
    while i < end:
        i = i + 1
        total_n = total_n + i

# 主程序
print(compute_sum(100))

屏幕显示:NoneNone是python语言中的一个特殊的值,可简单解释为没有对象或值[^2]。
无需沮丧,我们接近成功了。这首先说明函数自身也是有值的,称为函数值,目前compute_sum()函数的值是None;其次说明我们的思路符合python语言设计的意图,函数可以有值,更进一步,函数值可以是任意对象/值。
键入如下代码并观察执行结果。

# 函数定义
def compute_sum(end):
    i = 0
    total_n = 0

    while i < end:
        i = i + 1
        total_n = total_n + i

    return total_n

主程序
print(compute_sum(100))

终于得到了预期中的结果。
本段代码中的return是python的关键字。由returntotal_n赋值给函数compute_sum(),此时compute_sum()的函数值即为total_n当前的值。由于在得到函数的值以后,程序要返回到函数调用的位置,同时,函数的值也被顺利‘带回’,因此函数调用后得到的函数值,一般称为函数返回值
我们顺利的完成了真正意义上的第一个实用函数(前面那个fake_print原来真是假的,笑),但我们并没有特别激动。
一切本该如此,水到渠成,顺理成章。

4.4 拓展与总结 (定义、以及图形等均可在这里)

  • return语句。
    一个函数根据需要可以有0,1或多个return语句。
    键入如下代码并观察执行结果。
def example_0(number):
    number += 10

def example_1(number):
    number += 10
    return

# 主程序
n=100
print('0:',example_0(n))
print('1:',example_0(n))

没有return语句或者return语句后面没有参数,调用函数后函数的值都是None。因此,在程序不需要返回值的时候,可以不用return语句。
键入如下代码并观察执行结果。

def example_multi(number):
    return '多个返回值', number*10, number, number/10


# 主程序
n=100
print(example_multi(100))

可见可以利用return语句返回多个返回值[^3],且返回值的类型可以不同。
键入如下代码观察执行结果。

def zero_flag(number):
    if number < 0:
        return -1
    elif number == 0:
        return 0
    else:
        return 1

# 主程序
print('First:', zero_flag(100))
print('Second:', zero_flag(0))
print('Third:', zero_flag(-100))

通过以上代码我们可以了解,在函数中,运行到return语句时,该return语句除了提供函数返回值以外,同时还会使程序跳出函数体(不会执行return语句之后函数体内的语句),直接返回到函数调用处。因此,即使一个函数有多个return语句,每次调用时,最多仅会执行某一个return语句。

  • 函数调用与作用域初探

我们已经实现了自定义函数,并在主程序中调用了自定义函数。在自定义函数中,多次对内置函数进行了调用。
一个自定义函数也可以调用另一个自定义函数。例如:

# 自定义函数调用示例

def my_abs(number):
    if number < 0:
        number = -number

    return number

def compute_sum(end):
    i = 0
    total_n = 0
    number = my_abs(end)

    while i < number:
        i = i + 1
        total_n = total_n + i

    return total_n


# 主程序
i=1000
print('1+2+...+', i, '=',compute_sum(i))

上述代码中,自定义求和函数compute_sum()调用自定义函数my_abs(),求得传入参数end的绝对值来作为累加的终点值,就此实现了自定义函数之间的调用。
细心的读者可能已经注意到一件事,在my_abs()函数与compute_sum()函数内,有相同的变量名number。在compute_sum()函数与主程序中,有相同的变量名i。但是注意,这几个名字相同的变量,并非同一个变量。

  • python程序可根据函数定义(0、1或多个)及主程序划分为多个区域,每个区域内都的变量(对象)的在各个区域的使用(作用)有一定规则限制,因此这种区域称作作用域,又分为函数作用域(对应函数体)与全局作用域(对应主程序)。
  • 每个函数内部的变量只属于该函数的函数体,主程序无法使用。例:
#函数作用域示例1

def inner(n):
    n = 10
    print('inner', n)

#主程序
inner(n)        #报错

将提示句法错误:name 'n' is not defined,即n没有定义。虽然在主程序前面的inner()函数定义中,变量n被赋值10,但主程序无法使用inner()函数中的变量n,因此报错。

  • 每个函数内部的变量,其他函数也无法使用。例:
#函数作用域示例2

def inner1(n):
    n = 10
    print('inner', n)

def inner2():
    print(n)

#主程序
inner1(10)
inner2()        #报错

所提示的出错信息与前一段示例类似。
因此函数内部变量一般称为局部变量

  • 自定义函数能够读取并使用主程序(同一个程序中的)中变量的值,因此主程序中的变量,也称为全局变量。例:
#函数作用域示例3

def inner():
    print('inner', n)

#主程序

n=100
inner()

从示例3代码中可以看到,inner()函数内部并没有对n进行定义,但是能够从全局变量n中得到值100。但是自定义函数一般不能[^4]改变全局变量的值。例:

#函数作用域示例4

def inner():
    n=10
    print('inner', n)

#主程序

n=100
inner()
print('outer', n)

可知,虽然在自定义函数内部,将n赋值了10,但是在主程序中的全局变量n的值仍然是100
这个是怎么回事呢?是调用自定义函数inner()过程中,,全局变量n的值由100被赋值为10,然后从inner()函数返回的时候,n又被改回100了嘛?这似乎合理,但请看:

#函数作用域示例5

def inner():
    print('inner', n)
    n=10

#主程序

n=100
inner()
print('outer', n)

程序出现错误,local variable 'n' referenced before assignment,即局部变量n赋值前被使用/引用。示例4与5中,变量n被定义了(赋值就是一种定义),因此其中的n虽然与全局变量n同名,但其实是局部变量。
因此,示例4中使用的n是局部变量,其值为10,与全局变量n(值为100)无关;示例5中使用的n也是局部变量,在打印之前,并没有值,因此出错。只有示例3,由于函数内部没有对n进行定义,因此使用的是全局变量n的值。
最后要说明的一点是,对在自定义函数中与全局变量同名的变量,只要在自定义函数内部对该同名变量进行了任何形式[^4]的赋值,无论在该函数体内的哪个位置,该同名变量即成为局部变量,与同名全局变量无关。以下是更多示例:

#函数作用域示例6

def inner():
    print('inner', n)
    n=n

#主程序

n=100
inner()
print('outer', n)
#函数作用域示例7

def inner():
    n=n
    print('inner', n)

#主程序

n=100
inner()
print('outer', n)

最后一个示例比较有趣,请读者自行分析。

  • python内置函数

python语言解释器内置了一大批函数,可以直接使用。函数具体可见:https://docs.python.org/3/library/functions.html

本小节先介绍几个常用内置函数,后续还会陆续介绍。

  • abs(x)

Return the absolute value of a number. The argument may be an integer or a floating point number. If the argument is a complex number, its magnitude is returned.

  • divmod(a, b)

Take two (non complex) numbers as arguments and return a pair of numbers consisting of their quotient and remainder when using integer division. With mixed operand types, the rules for binary arithmetic operators apply. For integers, the result is the same as (a // b, a % b).

  • round(number[, ndigits])

Return the floating point value number rounded to ndigits digits after the decimal point. If ndigits is omitted or is None, it returns the nearest integer to its input.

4.5 完整代码

#求sigma1~m + sigma1~n + sigma1~k

def compute_sum(end):
    i = 0
    total_n = 0

    while i < end:
        i = i + 1
        total_n = total_n + i

    return total_n

n = int(input('请输入第1个整数,以回车结束。'))
m = int(input('请输入第2个整数,以回车结束。'))
k = int(input('请输入第3个整数,以回车结束。'))

print('最终的和是:', compute_sum(m) + compute_sum(n) + compute_sum(k))

4.6 实践与练习

  • 练习 1:仿照求$ \sum_{i=1}^mi + \sum_{i=1}^ni + \sum_{i=1}^ki$的完整代码,写程序,可求m!+n!+k!
  • 练习 2:写函数可返回1 - 1/3 + 1/5 - 1/7...的前n项的和。在主程序中,分别令n=1000及100000,打印4倍该函数的和。
  • 练习 3:将task3中的练习1及练习4改写为函数,并进行调用。
  • 挑战性练习:写程序,可以求从整数m到整数n累加的和,间隔为k,求和部分需用函数实现,主程序中由用户输入m,n,k调用函数验证正确性。

[^1] : 主程序可简单理解为指一个程序代码中第一层次的语句,该语句前没有空格,同时也不是函数定义语句。

[^2] : None这个对象不太容易清晰定义,举例进行说明会更容易理解。假设我们数东西(对象),如果数苹果,可以说1个苹果,2个苹果。当没有的时候,我们不说这是0个苹果,因为这时没有苹果这个对象存在;不能说这是0,因为我们不一定是在数数;也不能说没有苹果,因为我们不一定数什么我们只能说'没有'或'没有东西',这时候没有具体的对象和数。但是我们知道,如果确定了要数任何对象A,我们就可以说'没有A'。所以,python语言中,None可以被赋值给如函数及变量等任何对象。

[^3] : 此处多个返回值是在()里面,表示是python中元组(tuple)类型,将在后续章节介绍。

[^4] : 用global关键字,可以在函数内部改变全局变量的值,相对特殊,将在后续章节介绍。