子例程
函数是一组按顺序执行以影响某些结果的代码语句。在最一般的意义上,函数几乎可以是任何东西,例如平方根运算 例如,用给定的调色板绘制蒙娜丽莎(Mona Lisa)的指令,比对进化上距离遥远的生物体的同源蛋白的残留序列的指令,更新Facebook个人资料状态的指令,更新客户储蓄账户余额的指令,等等。
我们可以立即在上面列出的示例函数中看到一个除法:前三个论点(一个数字,一个调色板,氨基酸序列)并返回一个输出(一个数字,一幅画,一个序列对齐),而最后两个论点(文本字符串,一个事务)和改变一个变量,住在其他地方的代码库(副作用)。
纯函数
第一类函数(称为纯函数)以一种完全独立的方式接受输入并给出输出,而第二类函数接受输入,执行改变外部世界的操作,可能使用外部世界的其他东西。给定相同的输入,纯函数每次都会返回相同的答案。
很容易(相对地)推断出使用纯函数构建的程序,事实上,已经有完整的编程语言都是采用这种风格构建的(Haskell、Lisp、OCaml、f#等)。虽然副作用对于一个“做某事”的程序(例如用户界面)是必要的,但它们更难以理解,应该尽可能避免。
纯函数
纯函数是从一组输入到输出的映射
这样的每个元素 只有一个图像
在哪里 和 是数据、其他函数等的集合。
例如,可以定义一个函数,该函数接受两个数字( , ),并返回 提高到权力 .
权力
在OCaml中实现的,幂函数(对于正实数
x
和n
)是由
1让权力xn=x**n;;
此功能映射 没有副作用。
我们可能想要一个函数,主要的
,它取一个给定的整数并确定它是否是素数。为了达到这个目的,一个特别低效的算法是一个蛮力测试,测试是否能被小于或等于我们的整数的所有数字整除。
Python中的蛮力原语
1 2 3 4 5 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)
下面是克罗内克张量 的排名 .(在计算中,是克罗内克张量 的排名 是单位矩阵的顺序吗 ;也就是说,如果 ,然后 ,否则 .
1 2 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 5 5def阶乘(n):结果=1为数量在范围(1,n+1):结果=结果*数量返回结果
用递归
1 2 3 4 5 5def阶乘(n):如果n==1:返回1其他的:返回n*阶乘(n-1)
在不使用递归的情况下,我们必须利用一些令人讨厌的编程实践。首先,我们使用了一种全局变量
结果
,其值通过循环运行时更新。我们还必须指定一个范围,该范围为潜在的围栏(Off-One)错误。在递归情况下,我们只规定要件:基本情况和递归定义。在这里,任何错误都是显而易见的,因为它将来自数学定义,而不是来自我们实现的微妙细节。使用递归,我们更有可能得到干净的代码。
在这里,我们已经看到了递归对组织和避免错误的好处。但是,阶乘计算的结构非常简单,只有一串我们乘以一系列的数字。让我们尝试计算斐波纳契号,一个略裕的结构的问题。
斐波纳契数
就像我们处理整数阶乘函数一样,让我们写一个递归定义 Th Fibonacci号码:
例如,要找到第三个fibonacci号码,这个定义产量
对于第四个,我们有
我们可以在Python中实现这个函数,如下所示
1 2 3 4 5 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
.
默认值
每次调用函数时,通常会发现它们的一些参数有相同的值。或者,您可能有一个函数,如果需要的话,希望能够修改其行为,但在大多数情况下,不需要进行调整就可以运行该函数。在这两种情况下,为函数参数设置默认值是很方便的。
一个可选的论点有助于真正的爱
假设你正在编写一款在你忙的时候自动回复短信的应用程序。还假设存在某种方法,通过这种方法传入的文本消息可以触发函数,即您已经调用的函数
发短讯
.的默认返回值发短讯()
是“抱歉,现在不能谈,正在开会。”
.
1 2 3.def发短讯(数量):消息=“抱歉,现在不能谈,正在开会。”短信.消息(数量,消息)
这在大多数情况下都很有效,但可能会让你和你的另一半有麻烦,他们可能会认为你在探索其他选择,并忽视他们。在这种情况下,使用可选参数调用函数是很方便的
is_honeybear
表示你的另一半是接受者。在这种情况下是通话send_text (is_honeybear = True)
,提示应用程序附加消息以便读取“对不起,现在不能说话,在一次会议上。我非常爱你!”
.
1 2 3 4 5 5def发短讯(数量,is_honeybear=假):消息=“抱歉,现在不能谈,正在开会。”alt_message.=“我太爱你了!”发送=消息+(alt_message.如果is_honeybear其他的"")短信.消息(数量,消息)
在第一种情况下,当重要的人以外的人在打电话,
发短讯
使用默认值is_honeybear
这是假
.
在实践中,默认值往往在开始使用函数时显示出来。在设计阶段,通常使函数和参数尽可能简单,但在代码库中使用后,很明显有时需要重写某些值。
作为一个具体示例,假设您已经编写了一个在表格数据集中读取的函数(具有列名的电子表格),默认情况下,预计选项卡将分隔列:
1 2 3 4进口CSV.defread_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进口CSV.defread_data(文档名称,分隔器='\ t'):与文件(文档名称)作为《外交政策》:返回CSV..读者(《外交政策》,分隔符=分隔器)
因此,每当我们调用
read_data
在标签上分隔数据,我们只能提供文件名。当我们需要将分隔符指定为不同的东西时,例如,我们可以调用read_data('sample_data.txt',sepitator =',')
.