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
我们之前已经频繁的使用到了函数。
函数是一个带有输入和输出的能够完成特定功能的代码块。比如,在某段程序中我们可能需要频繁的使用某一功能,比如,我们可能需要频繁的计算正态分布的密度函数,虽然我们可以使用:
进行计算,但是以上计算公式仍然十分复杂,每次碰到都输入不仅麻烦,而且容易犯错。更方便的做法时将以上语句写成一个函数(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处的密度函数值,不能将顺序搞错。从而以下调用方式:
必然是错的,因为标准差不可能为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的值,只需要在调用时声明即可,比如:
就计算了标准差为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__进行调阅。
在创建函数时,必须注意变量的作用域问题。
作用域即变量的作用范围。比如,我们在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,则会按照函数体内的定义:
函数虽然好用,但是有一个缺点是,有时语句非常简单,但是我们还需要写def、return等语句。
Python提供了一个语法糖,即lambda表达式,可以让我们很方便的在一行以内定义一个函数。比如刚刚计算标准正态分布的密度函数的函数可以定义为:
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的函数中,计算返回值,再根据返回值排序。比如:
[('ter Stegen', 1), ('Pique', 3), ('Busquets', 5), ('Xavi', 6), ('Suárez', 9), ('Messi', 10)]
或者,我们可以使用lambda表达式写成更加简洁的形式:
可能有心的读者已经注意到了,函数是可以通过等号赋予某一个变量的,甚至可以传入另外一个函数。
实际上在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()函数,计算得到了具体数值。
递归(recursion)是函数是函数值编程一个非常有意思的应用,即一个函数调用其本身。
比如,为了计算阶乘,我们通常会使用循环:
如果使用递归的思路,我们可以这么写:
在以上程序中,我们定义了一个函数factorial(),接受一个数字作为参数:只要该参数不为1,就继续往下乘,直到编程1为止。因而,factorial(5)具体执行了:
接下来我们展示了一个二分查找的例子,可以证明,以下的二分查找比线性查找更加快速。
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
函数式编程另外比较常用的特性时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() 函数计算了一个列表的和:
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整除的数:
注意我们在使用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.前缀。
当然,不需要前缀的代价是要小心不要覆盖已经导入的东西,比如:
此外,import 语句还可以为导入的东西起别名,比如 import math as m,以后就可以使用m.sin()、m.e、m.pi、m.log()、m.exp()等替代math.sin() 等等。