第3章 过程大于结果

3.1 懒人炒菜机

1.函数是什么


数学上函数f(x)定义了两组数字之间的对应关系:

x -> y

1 1

2 4

3 9

4 16

对于程序员来说,函数是这样一种语法结构:它把一些指令封装在一起,形成一个组合拳。函数是对封装理念的实践。 输入数据被称为参数,参数能影响函数的行为。

2.定义函数


制作函数的过程又称为定义函数(definefunction)。这个函数的功能是计算两个数的平方和:

def square_sum(a,b):

    a = a**2

    b = b**2

    c = a + b

    return c

在定义函数square_sum()时,我们用参数a和b完成了符号化的平方求和。 最后一句return用于说明函数的返回值,即函数的输出数据。函数执行到return时就会结束,不管它后面是否还有其他函数定义语句。

在Python的语法中,return并不是必需的。 如果没有return, 或者return后面没有返回值时,则函数将返回None。 None是Python中的空数据,用来表示什么都没有。 关键字return也返回多个值。 多个值跟在return后面,以逗号分隔。 从效果上看,其等价于返回一个有多个数据的元组。

return a,b,c #                 相当于 return (a,b,c)

3.调用函数


a = 5

b = 6

x = square_sum(a, b)

print(x)                 # 结果为61

Python通过参数出现的先后位置,知道定义中的第一个形参a,和第二个形参b,然后把参数传递给函数square_sum()。 函数square_sum()执行内部的语句,直到得出返回值61。

4.函数文档


一个问题常见就是,我们经常会忘记一个函数是用来做什么的。我们可以用内置函数help()来找到某个函数的说明文档。 以函数max()为例,用这个函数用来返回最大值。 比如:

x = max(1, 4, 15, 8)

print(x) # 结果为15

>>> help(max) # 以下为help()运行的结果,也就是max()的说明文档。

Help on built-in function max in module __builtin__:


max(...)

max(iterable[, key=func]) -> value

max(a, b, c, ...[, key=func]) -> value


        With a single iterable argument, return its largest item.

        With two or more arguments, return the largest argument.

(END)

可以看到,函数max()有两种调用方式。 我们之前的调用是按照第二种方式。 对于我们自定义的函数,还需要自己动手添加函数说明文档。 下面给函数square_sum()加上简单的注释:

def square_sum(a,b):

        """return the square sum of two arguments"""

        a = a**2

        b = b**2

        c = a + b

        return c

>>>help(square_sum)

Help on function square_sum in module __main__:

square_sum(a, b)

        return the square sum of two arguments

3.2 参数传递

1.基本传参


把数据用参数的形式输入到函数,被称为参数传递。 如果有多个参数,那么在调用函数时,Python会根据位置来确认数据对应哪个参数,比如:

def print_arguments(a, b, c):

        """print arguments according to their sequence"""

        print(a, b, c)

print_arguments(1, 3, 5)                 # 打印1、 3、 5

print_arguments(5, 3, 1)                 # 打印5、 3、 1

print_arguments(3, 5, 1)                 # 打印3、 5、 1

在定义函数时,我们给了形参一个符号标记,即参数名。 关键字传递是根据参数名来让数据与符号对应上。 因此,如果在调用时使用关键字传递,那么不用遵守位置的对应关系。

print_arguments(c=5,b=3,a=1)                 # 打印1、 3、 5

位置传递与关键字传递可以混合使用,但如果把位置参数1放在关键字参数c=5的后面,则Python将报错。

print_arguments(1, c=5,b=3)                 # 打印1、 3、 5

print_arguemnts(c=5, 1, b=3)                 # 程序报错

但在函数定义时,我们可以设置某些形参的默认值。 如果我们在调用时

不提供这些形参的具体数据,那么它们将采用定义时的默认值。

def f(a,b,c=10):

        return a+b+c

print(f(3,2,1))                 # 参数c取传入的1。 结果打印6

print(f(3,2))                 # 参数c取默认值10。 结果打印15

2.包裹传参


有时在定义函数时,我们并不知道参数的个数。这时候,用包裹(packing)传参的方式来进行参数传递会非常有用。为了提醒Python参数all_arguments是包裹位置传递所用的元组名,我们在定义package_position()时要在元组名all_arguments前加*号。下面是包裹位置传参的例子:

def package_position(*all_arguments):

        print(type(all_arguments))

        print(all_arguments)

package_position(1,4,6)

package_position(5,6,7,1,2,3)

参数all_arguments是包裹关键字传递所用的字典,因此在all_arguments前加**

下面是包裹关键字传递的例子。

def package_keyword(**all_arguments):

        print(type(all_arguments))

        print(all_arguments)

package_keyword(a=1,b=9)

package_keyword(m=2,n=1,c=11)

包裹位置传参和包裹关键字传参还可以混合使用:

def package_mix(*positions, **keywords):

        print(positions)

        print(keywords)

package_mix(1, 2, 3, a=7, b=8, c=9)

包裹传参和基本传参可以混合使用。 它们出现的先后顺序是:位置→关键字→包裹位置→包裹关键字。

3.解包裹


*和**可用于函数调用实现一种叫作解包裹(unpacking)的语法。 解包裹允许我们把一个数据容器传递给函数,再自动地分解为各个参数。包裹传参和解包裹并不是相反操作,而是两个相对独立的功能。 例如:

def unpackage(a,b,c):

        print(a,b,c)

args = (1,3,4)

unpackage(*args)                 # 结果为1 3 4

args = {"a":1,"b":2,"c":3}

unpackage(**args)                 # 打印1、 2、 3

我们通过在args前加上*符号,来提醒Python,我想把元组拆成三个元素,每一个元素对应函数的一个位置参数。于是,元组的三个元素分别赋予了三个参数。 通过在args前面加上**符号提醒Python传递词典args,让词典的每个键值对作为一个关键字传递给函数unpackage()。解包裹用于函数调用时依然是相同的基本原则:位置→关键字→位置解包裹→关键字解包裹。

3.3 递归

1.高斯求和与数学归纳法


老师说必须算出1到100的和才能回家。 7岁的高斯想出了一个聪明的解决办法,后来这个方法被称为高斯求和公式。

sum = 0

for i in range(1, 101):                 # range()这样的写法表示从1开始,直到100

sum = sum + i

print(sum)                 # 结果为5050

我们还可以用了递归法。递归(Recursion),即在一个函数定义中,调用了这个函数自身。递归的关键是说明紧邻的两个步骤之间的衔接条件。 尽管整个递归过程很复杂,但在编写程序时,我们只需关注初始条件、 终止条件及衔接,而无须关注具体的每一步。 计算机会负责具体的执行。

def gaussian_sum(n):

        if n == 1:

                return 1

        else:

                return n + gaussian_sum(n-1)

print(gaussian_sum(100))                 # 结果为5050

递归源自数学归纳法: 

第一步 证明命题对于n = 1成立。

第二步 假设命题对于n成立,n为任意自然数,则证明在此假设下,命题对于n+1成立。

命题得证

2.函数栈


程序中的递归需要用到栈(Stack)这一数据结构。 栈最显著的特征是“后进先出” (LIFO,Last In,First Out)。就像我们往箱子里存放一叠书时,每一本书,也就是栈的每个元素,称为一个(frame)。 栈只支持两个操作:pop和 push。 栈用弹出(pop)操作来取出栈顶元素,用推入(push)操作将一个新的元素存入栈顶。

为了计算gaussian_sum(100),我们需要先暂停gaussian_sum(100),开始gaussian_sum(99)的计算。每次函数调用时,我们在栈中推入一个新的帧,用来保存这次函数调用的相关信息。 栈不断增长,直到计算出gaussian_sum(1)后,我们又会恢复计算gaussian_sum(2)、gaussian_sum(3),……。

所以,程序运行的过程,可以看作是一个先增长栈后消灭栈的过程。 每次函数调用,都伴随着一个帧入栈。 如果函数内部还有函数调用,那么又会多一个帧入栈。 当函数返回时,相应的帧会出栈。 等到程序的最后,栈清空,程序就完成了。

3.变量的作用域


 在下面的程序中,主程序和函数external_var()都有一个info变量。 在函数external_var()内部,会优先使用函数内部的那个info,且函数内部使用的是自己内部的那一份,所以函数内部对info的操作不会影响到外部变量info。

def external_var():

        info = "Vamei's Python"

        print(info)                 # 结果为"Vamei's Python"

info= "Hello World!"

external_var()

print(info)                 # 结果为"Hello World!"

在函数调用时,会把数据赋值给这些变量。 等到函数返回时,这些参数相关的变量会被清空。 但也有特例,如:

b = [1,2,3]

def change_list(b):

        b[0] = b[0] + 1

        return b

print(change_list(b))                 # 打印[2, 2, 3]

print(b)                 # 打印[2, 2, 3]

3.4 引入那把宝剑

1.引入模块


在Python中,一个.py文件就构成一个模块。 通过模块,你可以调用其他文件中的函数。 而引入(import)模块,就是为了在新的程序中重复利用已有的Python程序。我们先写一个first.py文件,再在同一目录下写一个second.py文件。 在这段程序中引入first模块:

def laugh():

        print("HaHaHaHa")

from first import laugh

for i in range(10):

        laugh()

借着import语句,我们可以在second.py中使用first.py中定义的laugh()函数。 除了函数,我们还可以引入其他文件中包含的数据。把常见的功能编到模块中,方便未来使用,就成为所谓的库(library)。

2.搜索路径

Python还会到其他的地方寻找库:

(1)标准库的安装路径

(2)操作系统环境变量PYTHONPATH所包含的路径

标准库是Python官方提供的库。 Python会自动搜索标准库所在的路径。 因此,

Python总能正确地引入标准库中的模块

3.5 异常处理

1.恼人的bug


工程师很早就开始用bug这个词来指代机械缺陷。 bug也被用于指代程序缺陷。如语法错误

for i in range(10)

        print(i)

SyntaxError: invalid syntax

编译器才会发现的错误被称为运行时错误(RuntimeError)

a = [1, 2, 3]

print(a[3])

IndexError: list index out of range

还有一种错误,称为语义错误(Semantic Error)。比如想打印第一个元素,却打印了第二个元素。

bundle = ["a", "b", "c"]

print(bundle[1])

2.Debug

修改程序缺陷的过程称为debug。 对于初学者来说,不需要花太多的时间在这些工具上。 在程序内部插入简单的print()函数,就可以查看变量的状态以及运行进度。

从另一个方面来看,debug也是写程序的一个自然部分。 有一种开发程序的方式,就是测试驱动开发(Test-Driven Development,TDD)。debug其实是你写出完美程序的一个必要步骤。

3.异常处理


对于运行时可能产生的错误,我们可以提前在程序中处理。 这样做有两个可能的目的:一个是让程序中止前进行更多的操作,比如提供更多的关于错误的信息。另一个则是让程序在犯错后依然能运行下去。

异常处理还可以提高程序的容错性。 下面的一段程序就用到了异常处理:

while True:

        inputStr = input("Please input a number:") # 等待输入

        try:

                num = float(inputStr)

                print("Input number:", num)

                print("result:", 10/num)

        exceptValueError:

                print("Illegal input.Try Again.")

        exceptZeroDivisionError:

                print("Illegal devision by zero.Try Again.")

若except后面没有任何参数那么表示所有的exception都交给这段程序处理:

while True:

        inputStr = input("Please input a number:")

        try:

                num = float(inputStr)

                print("Input number:", num)

                print("result:", 10/num)

        except:

                print("Something Wrong.Try Again.")

使用raise关键字可以在程序中主动抛出异常:

raiseZeroDivisionError()

你可能感兴趣的:(第3章 过程大于结果)