子例程
函数是一组按顺序执行的代码语句,以影响某些结果。在最一般的意义上,一个函数可以是几乎任何东西,例如平方根运算 例如,用给定的调色板画蒙娜丽莎的指令,在进化上相隔很远的有机体中排列同源蛋白质的残留序列的指令,更新你Facebook个人资料状态的指令,更新客户储蓄账户余额的命令,等等。
我们立即可以在上面列出的示例函数中看到一个划分:前三个函数接受一个参数(一个数字、一个调色板、氨基酸序列)并返回一个输出(一个数字、一幅画、一个序列对齐),而后两个函数接受一个参数(文本字符串、一个事务)并对代码库中其他位置的变量进行更改(一个副作用)。
纯函数
第一种函数(称为纯函数)接受输入并以一种完全自包含的方式给出输出,而第二种函数接受输入,执行一个操作来改变外部世界的东西,可能使用来自外部世界的其他东西。给定相同的输入,纯函数每次都会返回相同的结果。
推断使用纯函数构建的程序是很容易的(相对而言),事实上有完整的编程语言是为这种风格构建的(Haskell、Lisp、OCaml、f#等)。虽然副作用对于一个“做某事”(即用户界面)的程序是必要的,但它们更难以推理,应该尽可能避免。
纯函数
纯函数是从一组输入到一个输出的映射
这样每一个元素 有一个且只有一个图像
在哪里 而且 是数据集、其他函数集等。
例如,我们可以定义一个函数接受两个数字( , ),并返回 提升到幂 .
权力
在OCaml中实现,幂函数(对于正实数
x
而且n
)由
1让权力xn=x**n;;
这个函数映射 而且没有副作用。
我们可能想要一个函数,主要的
,它接受一个给定的整数并确定它是否是素数。实现这一点的一个特别低效的算法是强力测试能否被所有小于或等于整数的数整除。
Python中的蛮力质数
1 2 3 4 5def主要的(候选人):为数量在范围(2,候选人):如果候选人%数量= =0:返回假返回真正的
这个函数映射 而且没有副作用。如果 对于任何数量 ,我们的函数跳出
为
循环,返回假
,终止函数。如果它在列表中没有找到任何除数,则循环结束时不返回任何内容,并且还真
执行语句。
副作用
假设全局变量
一个
初始设置为7
,
1一个=7
我们有两个函数
add_a
,它返回值x +一个
,square_a
,重新分配的平方一个
来一个
.
1 2 3 4 5 6defadd_a(x):返回x+一个defsquare_a():全球一个一个=一个*一个
如果我们称
add_a (3)
没有调用square_a
,我们会10
,每次我们调用它。但是,如果我们打电话square_a
首先,add_a (3)
将返回52
,如果我们打电话square_a
两次,add_a (3)
将返回2404
.这是一个事实的结果add_a
取决于全局变量一个
这不是一个显式的论证add_a
.这使得函数容易受到一个
的范围之外的add_a
.如果我们写一个由调用组成的程序add_a
而且square_a
,结果关键取决于调用函数的顺序。因此,为了推理关于的输出
add_a
,我们需要知道我们环境的全部历史,具体来说,有多少次square_a
被称为。公然使用全局变量可能导致调试需要全面的取证分析的情况。我们可以通过显式地显示函数依赖关系,从而将调试任务隔离到单个函数,并促进更模块化的编程风格,从而在很大程度上避免这些问题。
变量和范围
在编写函数时,重要的是要清楚在不同级别的代码中可以访问哪些变量。如果我们试图在顶层引用一个在代码中更深层次定义的变量,或者期望对变量进行某种更改,那么就会遇到问题。
克罗内克符号
例如,考虑函数
克罗内克(n)
这意味着返回克罗内克张量 的排名 .(在计算中,克罗内克张量 的排名 是不是简单的单位矩阵 ;也就是说,如果 ,然后 ,否则 .
12 3 4 5 6 7 8 9 10 11 12 13 14def克罗内克(n):张量=[]为我在范围(n):行=[]为j在范围(n):def计算(我,j):如果我= =j:var=1其他的:var=0计算(我,j)行.附加(var)张量.附加(行)返回张量
如果我们称
克罗内克(3)
,我们期望得到
1 2 3[[1,0,0),[0,1,0),[0,0,1]]
但是,如果我们打电话
克罗内克(3)
如上所述,我们发现有一个范围错误。到底是哪里出了错?在每个循环中
j
索引,我们计算一个新的张量元素var
(使用函数计算(i, j)
)并将其添加到新形成的列表中行
.在每个循环结束时我
,行
是否被添加到列表中张量
,积累我们的成果。问题是我们期望
var
在的级别上定义j
中定义后循环计算(i, j)
.然而,var
是在代码的最深层定义的,因此不会在更高的级别定义。通常的约定是,变量可用于定义它们的同一级别的代码,以及低于该级别的所有级别的代码,但不能用于定义以上级别的代码(见下图)。
解决我们的
克罗内克
函数将
12 3 4 5 6 7 8 9 10 11 12def克罗内克(n):张量=[]为我在范围(n):行=[]为j在范围(n):如果我= =j:var=1其他的:var=0行.附加(var)张量.附加(行)返回张量
递归函数
函数不需要是有用代码片段的封装,这些代码片段的事件序列已经被显式地列出了。把这串数字循环一遍,然后把每个数字的平方打印出来。函数也可以定义为一组规则,以便任何给定输入映射到单个输出。通过这种方式,我们可以小心地在代码中定义计算,而将显式计算留给计算机。这种方法称为递归,通过声明一组已定义的基本用例来工作,根据这些基本用例可以计算函数的所有其他参数。
递归阶乘函数
对于一个经常被滥用但很清楚的例子,让我们考虑整数阶乘函数。定义的一种方法 是 ,例如,
这是一个很好的定义,但是写出来很乏味,特别是当我们的争论越来越大的时候。然而,在定义中有一个明确的模式 .考虑 如上所述,我们可以把它写成 .这种洞察力允许我们这样定义整数阶乘函数
为了了解这是如何简化代码的,让我们在Python中实现这两个阶乘。
在没有递归的情况下实现,我们有以下代码进行计算
1 2 3 4 5def的阶乘(n):结果=1为数量在范围(1,n+1):结果=结果*数量返回结果
用递归
1 2 3 4 5def的阶乘(n):如果n= =1:返回1其他的:返回n*的阶乘(n-1)
如果不使用递归,我们就不得不使用一些令人讨厌的编程实践。首先,我们使用了一种全局变量in
结果
,它的值随着循环的每一次运行而更新。我们还必须指定一个范围,这将为我们打开一个潜在的fencepost (off-by-one)错误。在递归情况下,我们只规定了基本情况:基本情况和递归定义。在这里,任何错误都是非常明显的,因为它将来自数学定义,而不是来自我们实现的微妙细节。使用递归,我们更有可能得到干净的代码。
这里我们已经看到了递归在组织和避免错误方面的好处。然而,阶乘计算的结构非常简单,只是一串我们乘在一起的数字。让我们试着计算斐波那契数列,一个结构更丰富的问题。
斐波纳契数
就像我们处理整数阶乘函数一样,让我们写一个递归定义 th斐波纳契数:
例如,要查找第三个斐波那契数列,这个定义产生
对于第四个,我们有
我们可以在Python中实现这个函数,如下所示
1 2 3 4 5def撒小谎(数量):如果数量在[0,1]:返回1其他的:返回撒小谎(数量-2)+撒小谎(数量-1)
这段代码可以很好地用于计算
心房纤颤(n)
当n
是小,然而,它是跛脚的缓慢n
生长。从我们上面尝试的案例中,我们可以看到计算的分支很快。让我们检查一下图表 在下面。在第一个分支,我们 .当 分支,它导致计算 而且 .然而,我们已经需要计算了 从第一个分支开始。一旦我们计算 只要一次,再做一次就没有什么意义了。
当我们计算越来越大的斐波那契数时,我们会发现越来越多这样的情况,树重复已经执行的计算(我们总共需要计算多少个数字?)除非我们将这些分支从树中删除,否则我们将浪费大量处理器时间重复我们已经做过的事情。
记忆有关
正如我们所注意到的,我们上面实现的递归斐波那契定义会导致大量的冗余计算。我们可以通过在第一次计算之后记住答案来节省时间。这种方式,心房纤颤(n)
的每个值将最多计算一次n
.这种停止计算先前获得的值的方法被称为“记忆”,通常可以用于获得显著的加速。
记忆递归斐波那契函数
首先,让我们创建一个查找表
fib_history
,并更改我们的代码,以便撒小谎
在提交一个新的计算之前查询这个表,也就是说,它只会执行一个新的分支,如果该分支的父分支之前没有被计算过。
1 2 3 4 5 6fib_history={1:1,0:1};deffib_memo(n):如果n不在fib_history:fib_history[n]=fib_memo(n-2)+fib_memo(n-1)返回fib_history[n]
这里我们可以看到
fib_memo
礼貌地问是否价值fib_memo (n)
还在fib_history
,只有当答案是no时,它才会进入递归。这样,我们只计算fib_memo
最多n
次在决定fib_memo (n)
.重新访问我们计算的树表示可以让我们看到节省的成本:
' '
fib_history
随着时间的推移积累知识,因此我们甚至可以使用之前调用的结果来帮助未来的调用。例如,如果我们打电话fib_memo (67)
在过去,我们不需要打68个电话就能找到fib_memo (68)
因为几乎所有的工作都已经完成了。下面,我们绘制
撒小谎
反对fib_memo
的适中值n
.很明显撒小谎
的运行时指数增长n
而fib_memo
这个范围内的成本是恒定的吗n
.
默认值
我们经常会发现,每次调用函数时,它们的某些参数往往具有相同的值。或者,您可能有一个函数,如果需要,您希望能够修改它的行为,但在大多数情况下不需要调整就可以运行它。在这两种情况下,为函数实参设置默认值是很方便的。
可选择的争论有助于真爱持久
假设你正在写一个应用程序,它可以在你忙的时候自动回复你的短信。还假设存在某种方法,通过该方法,传入的文本消息可以触发函数,即您已调用的函数
send_text
.的默认返回值send_text ()
是“对不起,现在不能说话,在开会。"
.
1 2 3defsend_text(数量):消息=“对不起,现在不能说话,在开会。"短信.消息(数量,消息)
这在大多数情况下都很有效,但可能会让你和你的另一半陷入麻烦,他们可能会认为你在寻找其他的选择,并忽略了他们。在这种情况下,调用带有可选参数的函数很方便
is_honeybear
表示你的另一半是收信人。在这种情况下,调用send_text (is_honeybear = True)
,提示应用程序添加消息以便读取“对不起,现在不能说话,在开会。我太爱你了!”
.
1 2 3 4 5defsend_text(数量,is_honeybear=假):消息=“对不起,现在不能说话,在开会。"alt_message=“我太爱你了!”to_send=消息+(alt_message如果is_honeybear其他的"")短信.消息(数量,消息)
在第一种情况下,除了另一半以外的人打电话给你,
send_text
使用默认值is_honeybear
这是假
.
在实践中,默认值往往会在开始使用函数时显示出来。在设计阶段,您通常使函数和参数尽可能简单,但在代码库中使用之后,很明显有时需要重写某些值。
作为一个具体的例子,假设你写了一个函数,它读取一个表格数据集(一个有列名的电子表格),默认情况下,期望用制表符分隔列:
1 2 3 4进口csvdefread_data(文件名):与文件(文件名)作为《外交政策》:返回csv.读者(《外交政策》,分隔符='\ t')
如果我们称
read_data
在像文件这样的数据集上sample_data.txt
1 2 3 4 5 6 7产品数量单价马车2 30.00铲1 10.00化肥22 1.50 . . . . . . . . .
它可以正常工作,因为分隔符的参数告诉我们
pandas.read_csv
如我们在文件中所见,期望列由制表符分隔。但是,您开始从一个新的客户端接收类似格式的电子表格进行分析,但不是用制表符分隔列,而是用逗号分隔列。因为任务非常相似,所以我们不需要编写单独的函数来处理逗号分隔和制表符分隔的数据。
我们可以通过给予来解决这个问题
read_data
分隔符的可选参数,分隔符
.为方便起见,可以将默认值设置为我们希望在导入数据集时使用的最常见参数——制表符分隔符\ t
.
1 2 3 4进口csvdefread_data(文件名,分隔符='\ t'):与文件(文件名)作为《外交政策》:返回csv.读者(《外交政策》,分隔符=分隔符)
因此,无论何时我们打电话
read_data
对于制表符分隔的数据,我们可以只提供文件名。当我们需要指定分隔符为不同的东西时,例如逗号,我们可以调用read_data (sample_data.txt,分隔符= " ")
.