子例程
函数是一组按顺序执行以影响某些结果的代码语句。在最一般的意义上,函数几乎可以是任何东西,例如平方根操作 例如,用给定的调色板画蒙娜丽莎的指令,在进化较远的生物中排列同源蛋白的剩余序列的指令,更新你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 + a
,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- 1)错误。在递归情况下,我们只规定基本情况和递归定义。在这里,任何错误都是非常明显的,因为它将来自数学定义,而不是来自我们实现的微妙细节。使用递归,我们更有可能得到干净的代码。
在这里,我们看到了递归在组织和避免错误方面的好处。然而,阶乘计算的结构非常简单,只是一串我们相乘的数字。让我们试着计算斐波那契数列,这是一个结构稍微丰富一些的问题。
斐波纳契数
就像处理整数阶乘函数一样,让我们为 第斐波那契数:
例如,要找到第三个斐波那契数,这个定义得到
第四个,我们有
我们可以在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
,只有当答案为“否”时,才会进入递归。这样,我们只计算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
对于TAB分隔的数据,我们可以只提供文件名。当我们需要指定分隔符为不同的东西时,例如逗号,我们可以调用read_data (sample_data.txt,分隔符= " ")
.