19  函数

我们之前已经频繁的使用到了函数。

函数是一个带有输入和输出的能够完成特定功能的代码块。比如,在某段程序中我们可能需要频繁的使用某一功能,比如,我们可能需要频繁的计算正态分布的密度函数,虽然我们可以使用:

pi=3.141592654
e=2.718
sigma=1
mu=0
f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma^2))

进行计算,但是以上计算公式仍然十分复杂,每次碰到都输入不仅麻烦,而且容易犯错。更方便的做法时将以上语句写成一个函数function)。

在Python中,函数可以方便的使用def来定义,比如,上面计算正态分布的密度函数的函数可以写为:

def normden(x,mu,sigma):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f
print(normden(0,0,1))
0.39894228037538715

在以上语句中,normden是所声明函数的函数名称,后面括号中的“x”、“mu”、“sigma”都是需要用户输入的参数:我们希望计算期望为mu,标准差为sigma的正态分布在x处的密度函数值。

在函数的最后,有一个return语句,该语句用以给出函数的返回值:在本例中就是我们计算得到的密度函数值。

注意如果需要函数有返回值,必须加入return语句,如果不加,函数默认返回None:

def normden(x,mu,sigma):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    # return f
print(normden(0,0,1))
None

此外需要注意的时,在调用函数时必须按照声明函数时的参数顺序传入参数,比如在上例中,normden(0,0,1)即对于期望为0,标准差为1的正态分布,计算x=0处的密度函数值,不能将顺序搞错。从而以下调用方式:

normden(0,1,0)

必然是错的,因为标准差不可能为0。

有时对于一个足够复杂的函数,可能会有很多参数值需要传入,而有些参数是允许有默认值的,即使用函数时如果不需要改动可以不改动。此时,我们可以可以在声明函数时就给出默认值,比如,在上例中,如果我们默认计算标准正态分布(期望为0、标注差为1)的密度函数,可以这样写:

def normden(x,mu=0,sigma=1):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f
print(normden(0))
0.39894228037538715

如果需要指明mu或者sigma的值,只需要在调用时声明即可,比如:

print(normden(0,sigma=2))
0.19947114018769357

就计算了标准差为2、期望为0的正态分布的密度函数值。

此外,我们之前提到过,为代码加注释是非常好的习惯,在Python中,由于自定义的函数有可能被其他人使用,而其他人不一定会看源代码,因而注释可能不会被看到。Python提供了一种非常方便的方法,即文档字符串docstring),这个字符串仅仅是在函数体内、放在函数开头、不赋值给任何变量的字符串:

def normden(x,mu=0,sigma=1):
    "Function computing the density function of normal distribution with expection mu and variance sigma at x."
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f
print(normden.__doc__)
Function computing the density function of normal distribution with expection mu and variance sigma at x.

声明了文档字符串后,可以直接使用function.__doc__进行调阅。

19.1 作用域

在创建函数时,必须注意变量的作用域问题。

作用域即变量的作用范围。比如,我们在normden()函数中定义了两个变量:pi和e,但是由于这两个变量是在函数体内定义的,因而外界无法使用这两个变量:

def normden(x,mu=0,sigma=1):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f
try:
    print(pi)
except Exception as e:
    print("错误:",e)
错误: name 'pi' is not defined

可见程序出错,并提示pi这个变量没有定义。

然而反过来,被定义在函数体外的变量,可以被函数所使用,比如,如果我们写成:

pi=3.141592654
e=2.718
def normden(x,mu=0,sigma=1):
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    print("函数体内,pi=",pi)
    return f
try:
    print("函数体外,pi=",pi)
    print(normden(0))
except Exception as e:
    print("错误:",e)
函数体外,pi= 3.141592654
函数体内,pi= 3.141592654
0.39894228037538715

不过,如果在函数体内重新定义了pi,则会按照函数体内的定义:

pi=4
e=2.718
def normden(x,mu=0,sigma=1):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    print("函数体内,pi=",pi)
    return f
try:
    print("函数体外,pi=",pi)
    print(normden(0))
except Exception as e:
    print("错误:",e)
函数体外,pi= 4
函数体内,pi= 3.141592654
0.39894228037538715

19.2 lambda表达式

函数虽然好用,但是有一个缺点是,有时语句非常简单,但是我们还需要写def、return等语句。

Python提供了一个语法糖,即lambda表达式,可以让我们很方便的在一行以内定义一个函数。比如刚刚计算标准正态分布的密度函数的函数可以定义为:

stdnormden=lambda x: 1/((2*3.141592654)**0.5)*2.718**(-1*(x**2)/2)
print(stdnormden(0))
0.39894228037538715

当然,简单的代价是损失方便性,上面的lambda表达式虽然定义了标准正态分布的密度函数,但是并没有定义一般意义的正态分布的密度函数,如果需要传入额外的变量,可以使用:

normden=lambda x, mu, sigma: 1/((2*3.141592654)**0.5*sigma)*2.718**(-1*((x-mu)**2)/(2*sigma**2))
print(normden(0,0,2))
0.19947114018769357

在这里,我们不妨回忆一下上一节中有关列表排序的内容,比如对于列表

player_list=[('Messi', 10), ('Xavi', 6), ('ter Stegen', 1), ('Busquets', 5), ('Pique', 3), ('Suárez', 9)]
print(player_list)
[('Messi', 10), ('Xavi', 6), ('ter Stegen', 1), ('Busquets', 5), ('Pique', 3), ('Suárez', 9)]

现在该列表的每个元素是一个元组,我们希望的是对每个元组的第二个元素排序。而列表的sort()函数中,key是一个参数,当这个参数不传入任何东西时,默认按照元素值进行排序;但是key参数允许传入一个函数,在进行排序时,先将每个元素带入到key的函数中,计算返回值,再根据返回值排序。比如:

def sort_list_by_2(t):
    return t[1]
player_list.sort(key=sort_list_by_2)
print(player_list)
[('ter Stegen', 1), ('Pique', 3), ('Busquets', 5), ('Xavi', 6), ('Suárez', 9), ('Messi', 10)]

或者,我们可以使用lambda表达式写成更加简洁的形式:

player_list.sort(key=lambda x: x[1],reverse=True)
print(player_list)
[('Messi', 10), ('Suárez', 9), ('Xavi', 6), ('Busquets', 5), ('Pique', 3), ('ter Stegen', 1)]

19.3 Python中的函数式编程

可能有心的读者已经注意到了,函数是可以通过等号赋予某一个变量的,甚至可以传入另外一个函数。

实际上在Python中也吸收了函数式编程的一些特点,函数也是“一等公民”,跟数字、字符串一样,也可以传给一个变量、当做参数传入给另外一个函数。比如:

def normden(x,mu=0,sigma=1):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f

normden2=normden
print(normden2(0))
print(normden2 is normden)
0.39894228037538715
True

上面的代码中,我们将已经声明的一个函数normden()赋值给了另一个变量,normden2,并使用normden2计算了0处的密度函数值。

此外,我们还是用is关键字判断两者是否指代一个对象,发现两者实际上指代的同一个对象,是完全等价的。

以下展示了将函数作为参数传递给另外一个函数:

def normden(x,mu=0,sigma=1):
    pi=3.141592654
    e=2.718
    f=1/((2*pi)**0.5*sigma)*e**(-1*(x-mu)**2/(2*sigma**2))
    return f

def den2(f,x):
    return f(x)**2

print(den2(normden,0))
0.159154943071114

以上程序中,我们定义了一个新的函数,den2(),该函数接受两个参数:f和x,其中f为一个函数,可用来计算f(x),并返回f(2)**2。

随后,我们将刚刚定义的normden()函数以及x=0传入了den2()函数,计算得到了具体数值。

19.3.1 递归

递归recursion)是函数是函数值编程一个非常有意思的应用,即一个函数调用其本身。

比如,为了计算阶乘,我们通常会使用循环:

def factorial(n):
    frcn=1
    for i in range(n):
        frcn*=(i+1)
    return frcn
print(factorial(5))
120

如果使用递归的思路,我们可以这么写:

def factorial(n):
    if n==1:
        return 1
    else:
        return n*factorial(n-1)
print(factorial(5))
120

在以上程序中,我们定义了一个函数factorial(),接受一个数字作为参数:只要该参数不为1,就继续往下乘,直到编程1为止。因而,factorial(5)具体执行了:

  1. factorial(5), 计算5*factorial(4)
  2. factorial(4), 计算4*factorial(3)
  3. factorial(3), 计算3*factorial(2)
  4. factorial(2), 计算2*factorial(1)
  5. factorial(1)=1
  6. 最终依次返回

接下来我们展示了一个二分查找的例子,可以证明,以下的二分查找比线性查找更加快速。

player_list=[('Messi', 10), ('Xavi', 6), ('ter Stegen', 1), ('Busquets', 5), ('Pique', 3), ('Suárez', 9)]
player_list.sort(key=lambda x:x[1])
def search(num,name_list):
    l=len(name_list)
    if l==1:
        if name_list[0][1]==num:
            return name_list[0][0]
        else:
            return None
    else:
        l2=l//2
        if name_list[l2-1][1]<num:
            return search(num,name_list[l2:])
        else:
            return search(num,name_list[0:l2])
print(search(10, player_list))
print(search(11, player_list))
Messi
None

19.3.2 map()、reduce()以及filter()

函数式编程另外比较常用的特性时map()、reduce()、filter等函数。

其中,map(func, list)函数接受一个函数func和一个列表list,其作用是将函数func作用与list的每一个元素中。

值得注意的是,map()语句之后,计算并不会立即执行,而是会返回一个可迭代的对象,等到需要计算值时才进行计算,比如:

num_list=list(range(10))
print(num_list)
num_sq_list=map(lambda x: x**2, num_list)
print(num_sq_list)
num_sq_list=list(num_sq_list)
print(num_sq_list)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
<map object at 0x7fb8acf3be48>
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

在以上程序中,我们先使用range()函数产生了一个0…9的列表,接着使用map函数,计算了每个元素的平方。注意到map()函数被调用之后,计算并没有立即执行,返回的是一个map对象。该对象是可迭代的,因而我们可以使用list()函数将其转化为列表。

或者,更常用的,我们可能会使用reduce()以及filter()函数处理map对象。其中:

  • reduce(func,iterable_obj)接受一个二元函数,以及一个可迭代的对象iterable_obj,该函数使用func函数依次计算iterable_obj每个的值。
    • 比如,如果iterable_obj=[x1,x2,x3,x4],那么reduce()函数计算的是:func(func(func(x1,x2),x3),x4)
  • filter(func,iterable_ojb)接受一个逻辑函数,以及一个可迭代的对象iterable_obj,该函数的作用是将iterable_obj中带入func返回值为True的子集挑出来。

比如,以下使用reduce() 函数计算了一个列表的和:

from functools import reduce

num_list=[1,3,8,6,9]
sums=reduce(lambda x, y: x+y, num_list)
print(sums)
27

以下使用reduce() 函数计算了一个列表的最大值:

from functools import reduce

num_list=[1,3,8,6,9]
sums=reduce(lambda x, y: max(x,y), num_list)
print(sums)
9

以下使用reduce() 函数计算了0…9的平方和:

num_list=list(range(10))
num_sq_list=map(lambda x: x**2, num_list)
sum_sq=reduce(lambda x,y:x+y, num_sq_list)
print(sum_sq)
285

以下则挑出了所有1,…,9的平方中的所有的能被3整除的数:

num_list=list(range(10))
num_sq_list=map(lambda x: x**2, num_list)
sub_sq=filter(lambda x:x%3==0, num_sq_list)
print(list(sub_sq))
[0, 9, 36, 81]

20 模块

注意我们在使用reduce()函数时,多加了一句:

from functools import reduce

原因是由于reduce()并非Python內建的函数,需要从functools这个模块中调用。functools这个模块是Python自带的,无需安装。

在Python中,模块module)通常是一个Python文件,在该文件中定义了很多函数、类、常数等供我们使用。

package)是一个目录,包中可以有很多的模块。

我们之前曾经讲过,使用类似“pip install bs4”之类的命令安装包,安装好之后就可以供我们在Python程序中调用。

如果需要使用某个模块,可以直接使用“import”命令。比如,我们经常使用的带有一些数学函数的math模块:

import math
print("e:", math.e)
print("pi:", math.pi)
print("sin(1):", math.sin(1))
print("log(2):", math.log(2))
e: 2.718281828459045
pi: 3.141592653589793
sin(1): 0.8414709848078965
log(2): 0.6931471805599453

注意在上面我们在使用math模块中的元素时,都加了math.的前缀,这是由于Python中的命名空间导致的,为了防止在不同的地方定义了相同的函数名等。比如:

import math
e=3
pi=4
print("e in math:", math.e)
print("e:", e)
print("pi in math:", math.pi)
print("pi:", pi)
e in math: 2.718281828459045
e: 3
pi in math: 3.141592653589793
pi: 4

为了防止混乱,特地使用math.前缀作为区分。

如果需要同时导入多个模块,可以直接用逗号隔开,比如以下程序我们使用梯形公式计算了标准正态分布函数从-1.96到1.96的数值积分:

import math,functools
def normden(x,mu=0,sigma=1):
    f=1/(math.sqrt(2*math.pi)*sigma)*math.exp(-1*(x-mu)**2/(2*sigma**2))
    return f

N=1000
x=[-1.96+(1.96*2)/N*i for i in range(N)]
# print(x)
integral=functools.reduce(lambda a,b: a+b, map(lambda z: normden(z)*(1.96*2)/N, x))
print("integral=", integral)
integral= 0.9500039163481592

注意到,我们使用import 语句同时导入了functools以及math两个模块,因而在这里我们需要使用functools.reduce来表示reduce()函数。

当然,我们也看到了,可以直接使用: from … import … 的语法来导入模块中的某一个元素,比如from functools import reduce就从functools模块中导入了reduce函数,且reduce函数使用时不需要再加functools.前缀。

当然,不需要前缀的代价是要小心不要覆盖已经导入的东西,比如:

from math import pi
print(pi)
pi=4
print(pi)
3.141592653589793
4

此外,import 语句还可以为导入的东西起别名,比如 import math as m,以后就可以使用m.sin()、m.e、m.pi、m.log()、m.exp()等替代math.sin() 等等。

import math as m
e=3
pi=4
print("e in math:", m.e)
print("e:", e)
print("pi in math:", m.pi)
print("pi:", pi)
e in math: 2.718281828459045
e: 3
pi in math: 3.141592653589793
pi: 4