抽象,自定义函数,递归

6.1懒惰是一种美德

如果你 在一个地方编写了一些代码,但需要在另一个地方再次使用,该如何办呢?

假设你编写了一段代码,它计算一些斐波那契数(一种数列,其中每个数都是前两个数的和)。

现在的数列包含10个斐波那契数。

如果要使用这些数字做其他事情,该如何办呢?我们肯定不愿意再重新写一遍代码。我们可以让程序更抽象。要让前面的程序更抽象,可以像下面这样做:

大家可以看见,我在确定num后直接可以打印出fibs。其实如果把整体封装起来,就可以直接调用。

6.2抽象和结构

抽象可节省人力,但实际上还有个更重要的优点:抽象是程序能够被人理解的关键所在。

人和计算机的差别在于,计算机本身喜欢具体而明确的指 令,但人通常不是这样的。

例如:如果你向人打听怎么去电影院,就不希望对方回答:"向前走10步,向左转90度,接着走5步,再向右转45度,然后走123步。"(这是计算机希望听见的)。"沿这条街往前走,看到过街天桥后走到马路对面,电影院就在你左边。"(这是人希望听见的)。这里的关键是你知道如何沿街往前走,也知道如何过天桥,因此不需要有关这些方面的具体说明。

组织计算机程序时,你也采取类似的方式。程序应非常抽象,如下载网页、计算使用频率、打印每个单词的使用频率。

看到这些代码,任何人都知道这个程序是做什么的。但是其中到底如何做,其中的细节会在其他地方给出。

6.3自定义函数

函数执行特定的操作并返回一个值,你可以调用它(调用时可能需要提供一些参数------放在圆括号中的内容)。一般而言,要判断某个对象是否可调用,可使用内置函数callable。

此时说明x是不可以调用的,y是可以的。(并非所有的函数都返回值)

函数是结构化编程的核心。那么如何定义函数呢?使用def(表示定义函数)语句。

上面两行是对于函数的简单定义,最下面是对函数的调用。

那现在回到斐波那契数,如果编写一个函数,返回一个由斐波那契数组成的列表呢?

前5行都是对于斐波那契数整个函数的定义,最后一行是对其进行调用。

其实我们不难发现,除了def一直需要外,我们在每个定义def中都有一个return语句。return语 句用于从函数返回值(在前面的hello函数中,return语句的作用也是一样的)。

6.3.1给函数编写文档

要给函数编写文档,以确保其他人能够理解,可添加注释(以#打头的内容)。

放在函数开头的字符串称为文档字符串(docstring),将作为函数的一部分存储起来。

代码前三行是定义,其中包含了我们的注释。最后一行我们先是调用了下函数进行计算,同时也读取了函数定义的文档字符串。(注意 __doc__是函数的一个属性。)

特殊的内置函数help很有用。在交互式解释器中,可使用它获取有关函数的信息,其中包含函数的文档字符串。

6.3.2其实并不是函数的函数

数学意义上的函数总是返回根据参数计算得到的结果。在Python中,有些函数什么都不返回。

什么都不返回的函数不包含return语句,或者包含return语句,但没有在return后面指定值。

这里虽然包含了return,但是其实return部分没有返回任何值,只是为了结束语句(第二个打印的go没有打印出来)(这有点像在循环中使用break,但跳出的是函数)

6.4参数魔法

函数使用起来很简单,创建起来也不那么复杂,但要习惯参数的工作原理就不那么容易了。

6.4.1值从哪里来

定义函数时,你可能心存疑虑:参数的值是怎么来的呢?

编写函数旨在为当前程序(甚至其他程序)提供服务,你的职责是确保它在提供的参数正确时完成任务,并在参数不对时以显而易见的方式失败。参数通常不需要担心。

在def语句中,位于函数名后面的变量通常称为形参,而调用函数时提供的值称为实参。很多情况下我们会将实参称为值,以便将其与类似于变量的形参区分开来。

6.4.2我能修改参数吗

参数不过是变量而已,行为与你预期的完全相同。在函数内部给参数赋值对外部没有任何影响。

在try_to_change内,将新值赋给了参数n,但如你所见,这对变量name没有影响。说到底,这是一个完全不同的变量。

这样可能更加直观,在有返回值的情况下,在定义里面修改参数会导致返回值的变化,但是但是重要的是它不会去修改我们的name。变量n变了,但变量name没变。同样,在函数内部重新关联参数(即给它赋值)时,函数外部的变量不受影响(参数存储在局部作用域内)。

字符串(以及数和元组)是不可变的(immutable),这意味着你不能修改它们(即只能替换为新值)。因此这些类型作为参数没什么可说的。但如果参数为可变的数据结构(如列表)呢?

唉?例子也是在函数内修改了参数,为什么函数外部的names发生变化了?大家先看下面:

这样的情况大家应该都见过,将同一个列表赋给两个变量时,这两个变量将同时指向这个列表。就这么简单。

这就解释了为什么在函数外的结果被修改了。

要避免这样的结果,必须创建列表的副本。对序列执行切片操作时,返回的切片都是副本。因此,如果你创建覆盖整个列表的切片,得到的将是列表的副本。

此时就可以发现,n和names虽然内容相同,但是已经不是同一个列表了。 注意到参数n包含的是副本。

(1)为何要修改参数

在提高程序的抽象程度方面,使用函数来修改数据结构(如列表或字典)是一种不错的方式。

假设你要编写一个程序,让它存储姓名,并让用户能够根据名字、中间名或 姓找人。为此,你可能使用一个类似于下面的数据结构:

每个键下都存储了一个人员列表。在这个例子里,这些列表只包含作者。

现在,要获取中间名为Lie的人员名单,可像下面这样做:

但是,如你所见,将人员添加到这个数据结构中有点繁琐,在多个人的名字、中间名或姓相同时尤其如此,因为在这种情况下需要对存储在名字、中间名或姓下的列表进行扩展。

下面来添加我的妹妹,并假设我们不知道数据库中存储了什么内容。

storage['first']['Anne'] 在第一个代码的第二行给first键对应的'Anne'键对应的值用my_sister添加,由于之前我们并没有对Anne的键对应相应的值,现在直接添加,形成的结果就是刚刚添加的。对于三行的Lie之前我们有设置过值,现在append一个,最后得到的结果就是第二个代码出现的结果。

可以想见,编写充斥着这种更新的大型程序时,代码将很快变得混乱不堪。

抽象的关键在于隐藏所有的更新细节,为此可使用函数。下面首先来创建一个初始化数据结构的函数。

你所见,这个函数承担了初始化职责,让代码的可读性高了很多。

接下来设计获取人员姓名的函数。

第一个代码就是获取设计人员姓名的函数。第二个代码最后就是对该函数的使用。

下面来编写将人员存储到数据结构中的函数。

运行一下:

如你所见,如果多个人的名字、中间名或姓相同,可同时获取这些人员。

(2)如果参数是不可变的

在很多语言中,经常需要给参数赋值并让这种修改影响函数外部的变量。在Python中,没法直接这样做,只能修改参数对象本身。

但是,如果参数不可变呢?没办法。在这种情况下,应从函数返回所有需要的值(如果需要返回多个值,就以元组的方式返回它们)。

这样没有进过修改参数

如果一定要修改参数,改变外部的值,可以把值放在列表中。

6.4.3关键字参数和默认值

有时候,参数的排列顺序可能难以记住,尤其是参数很多时。

但是当我们改成这样的时候参数的顺序就无关紧要了,但是名称变的很重要。

像这样使用名称指定的参数称为关键字参数,主要优点是有助于澄清各个参数的作用。

虽然这样做的输入量多些,但每个参数的作用清晰明了。另外,参数的顺序错了也没关系。

然而,关键字参数最大的优点在于,可以指定默认值。

像这样,设定完默认值后,调用时不提供它,也可以运行出来。

当然也可以提供默认值。提供1个或者2个都可以。

如你所见,这两个提供的值,其实还是按照位置进行分配的。那如何使用提供的name或者greeting。

还不止这些。你可结合使用位置参数和关键字参数,但必须先指定所有的位置参数,否则解释器将不知道它们是哪个参数(即不知道参数对应的位置)。通常不会结合位置参数和关键字参数。

大家可以自己看一下代码,思考一下与预期是否相同。

直接运行报错,因为name没有设定默认值。

6.4.4收集参数

有时候,允许用户提供任意数量的参数很有用。

每次只能存储一个数据太少了,如果可以同时存储多个数据就比较好:

为此,应允许用户提供任意数量的姓名。请尝试使用下面这样的函数定义:

这里好像只指定了一个参数,但它前面有一个星号。

看,我们引用这个定义,发现最终打印出的是一个元组,因为里面有一个逗号。那继续尝试是否可以打印出更多的项。

看出来,打印多个项目也是可以的。参数前面的星号将提供的所有值都放在一个元组中,也就是将这些值收集起来。这样的行我们在5.2.1节见过:赋值时带星号的变量收集多余的值。它收集的是列表而不是元组中多余的值,但除此之外,这两种用法很像。

和我们预期相同,第一个参数收集一个值(params),第二个参数由于有*号,所以收集剩余的值(1,2,3)。

这段代码,在第一个参数收集完第一个值,没有值了,所以第二个参数返回了一个空的元组。

那此时,我们可能会好奇,那如果说星号的参数在中间,它应该怎么判断剩余的元组呢?我们尝试一下:

星号不会收集管自己参数,如果不给z=6呢?

报错了。

星号不会收集关键字参数。如下。

正常来说,在参数title收集到Hmm...后,剩余内容被星号参数收集,但是事实上没有,因为提供了关键字了,星号不会收集关键字参数。

要收集关键字参数,可使用两个星号。

如你所见,这样得到的是一个字典而不是元组。

将以上的内容全部结合一下,位置,关键字,*,**

现在我们来解决最初的问题,如何在姓名存储示例中使用这种技术?解决方案如下:

6.4.5分配参数

前面介绍了如何将参数收集到元组和字典中,但用同样的两个运算符(*和**)也可执行相反的操作。与收集参数相反的操作是什么呢?假设有如下函数:

现在定义了一个相加的函数,但是给出的两个数据在一个元组里。现在就希望将这两个数据进行相加,但是定义中给的是两个参数。与收集参数相反的操作(不是收集参数,而是分配参数):

当然使用一个*可以,也可以使用两个**。可将字典中的值分配给关键字参数。如下:

大家看下面这一段代码:

对于函数with_stars,我在定义和调用它时都使用了星号,而对于函数without_stars,我在定义和调用它时都没有使用,但这两种做法的效果相同。

因此,只有在定义函数(允许可变数量的参数)或调用函数时(拆分字典或序列)使用, 星号才能发挥作用。

使用这些拆分运算符来传递参数很有用,因为这样无需操心参数个数之类的问题,如下所示:

6.5作用域

变量到底是什么呢?可将其视为指向值的名称。因此,执行赋值语句x = 1后,名称x指向值1。这几乎与使用字典时一样(字典中的键指向值),只是你使用的是"看不见"的字典。

有一个名为vars的内置函数,它返回这个不可见的字典:

警告 一般而言,不应修改vars返回的字典,因为根据Python官方文档的说法,这 样做的结果是不确定的。换而言之,可能得不到你想要的结果。

这种"看不见的字典"称为命名空间或作用域。那么有多少个命名空间呢?除全局作用域外,每个函数调用都将创建一个。

这个地方就有些疑问了,我调用foo了,foo函数内赋值语句x=42,为什么最终x没有变化,依然是1呢?

这是因为调用foo时创建了一个新的命名空间,供foo中的代码块使用。赋值语句x = 42是在这个内部作用域(局部命名空间)中执行的,不影响外部(全局)作用域内的x。在函数内使用的变量称为局部变量(与之相对的是全局变量)。参数类似于局部变量,因此参数与全局变量同名不会有任何问题。

简而言之,就是创建函数foo时,创建了新的命名空间,这个空间里(局部)的x值不会影响到全局作用域中的x值。因此局部变量与全局变量同名也没有问题。

读取全局变量的值通常不会有问题,但还是存在出现问题的可能性。

(1)"遮盖"的问题

如果有一个局部 变量或参数与你要访问的全局变量同名,就无法直接访问全局变量,因为它被局部变量遮住了。(其实就是在局部变量里面使用全局变量时,如果局部变量中有与全局同名的,系统不知道识别哪一个,而因为在定义里面,会先访问局部变量)

那我到底如何让函数里知道我要访问全局变量呢?使用global

看,此时函数里就访问到了全局变量的parameter。

还记得我们在6.5开始的时候,函数里面的值的赋值操作,并没有改变函数外的值。那如何改变呢?

重新关联全局变量(使其指向新值)是另一码事。在函数内部给变量赋值时,该变量默认为局部变量,除非你明确地告诉Python它是全局变量。当然还是使用global。

ok,大家知道了可以这样进行关联全局变量。大家可以思考一下为什么下面这个x还是1呢?

(2)作用域嵌套

Python函数可以嵌套,即可将一个函数放在另一个函数内。

嵌套通常用处不大,但有一个很突出的用途:使用一个函数来创建另一个函数。这意味着可像下面这样编写函数:

在这里,一个函数位于另一个函数中,且外面的函数返回里面的函数。也就是返回一个函数,而不是调用它。重要的是,返回的函数能够访问其定义所在的作用域。换而言之,它携带着自己所在的环境(和相关的局部变量)!

每当外部函数被调用时,都将重新定义内部的函数,而变量factor的值也可能不同。由于Python的嵌套作用域,可在内部函数中访问这个来自外部局部作用域(multiplier)的变量,如下所示:

第一句中的2其实就是multiplier的factor,double中的5,其实就是number。下面两个例子也是类似的。

像multiplyByFactor这样存储其所在作用域的函数称为闭包。

通常,不能给外部作用域内的变量赋值,但如果一定要这样做,可使用关键字nonlocal。这个关键字的用法与global很像,让你能够给外部作用域(非全局作用域)内的变量赋值。

6.6递归

你知道,函数可调用其他函数,但可能让你感到惊 讶的是,函数还可调用自己。

如果你以前没有遇到这种情况,可能想知道递归是什么意思。简单地说,递归意味着引用(这里是调用)自身。(其实完全没有必要明白递归的准确定义)

这就是一个递归式函数的定义,其实什么都没做,与"递归"定义一样傻。

从理论上说,这个程序将 不断运行下去,但每次调用函数时,都将消耗一些内存。因此函数调用次数达到一定的程 度(且之前的函数调用未返回)后,将耗尽所有的内存空间,导致程序终止并显示错误消 息"超过最大递归深度"。

这个函数中的递归称为无穷递归(就像以while True打头且不包含break和return语句的循环被称为无限循环一样),因为它从理论上说永远不会结束。

我们需要的是对我们有帮助的递归,通常包含两个部分:

基线条件(针对最小的问题):满足这种条件时函数将直接返回一个值。

递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分。

6.6.1 阶乘与幂(经典案例)

首先,假设你要计算数字n的阶乘。n的阶乘为n × (n - 1) × (n - 2) × ... × 1,在数学领域的用途非常广泛。

首先将result设置为n,再将其依次乘以1到n - 1的每个数字,最后返回result。但如果你愿意,可采取不同的做法。

ok,上面这个方法就一步步的进行计算,但我们也可以采用不同的方法。

①1的阶乘为1。

②对于大于1的数字n,其阶乘为n - 1的阶乘再乘以n。这与阶乘的定义相同。

再来看一个示例。假设你要计算幂,就像内置函数pow和运算符**所做的那样。要定义一个数字的整数次幂,有多种方式,但先来看一个简单的定义:power(x, n)(x的n次幂)是将数字x自乘n - 1次的结果,即将n个x相乘的结果。

同样,也可以将他改成递归式。

①对于任何数字x,power(x, 0)都为1。

②n>0时,power(x, n)为power(x, n-1)与x的乘积。

提示 如果函数或算法复杂难懂,在实现前用自己的话进行明确的定义将大有裨益。以这种"准编程语言"编写的程序通常称为伪代码。

那么使用递归有何意义呢?难道不能转而使用循环吗?答案是肯定的,而且在大多数情况下,使用循环的效率可能更高。然而,在很多情况下,使用递归的可读性更高,且有时要高得多,在你理解了函数的递归式定义时尤其如此。

6.6.2二分查找

例如,对方心里想着一个1~100的数字,你必须猜出是哪个。当然,猜100次肯定猜对,但最少需要猜多少次呢?实际上只需猜7次。首先问:"这个数字大于50吗?"如果答案是肯定的,再问:"这个数字大于75吗?"不断将可能的区间减半,直到猜对为止。你无需过多地思考就能成功。

这样的想法适用于众多其他不同的情形。

一个常见的问题是:指定的数字是否包含在已排序的序列中?如果包含,在什么位置?为解决这个问题,可采取同样的策略:"这个数字是否在序列中央的右边?"如果答案是否定的,再问:"它是否在序列的第二个四分之一区间内(左半部分的右边)?"依此类推。明确数字所处区间的上限和下限,并且每一个问题都将区间分成两半。

这里的关键是,这种算法自然而然地引出了递归式定义和实现。

①如果上限和下限相同,就说明它们都指向数字所在的位置,因此将这个数字返回。

②否则,找出区间的中间位置(上限和下限的平均值),再确定数字在左半部分还是右 半部分。然后在继续在数字所在的那部分中查找。

在这个递归案例中,关键在于元素是经过排序的。找出中间的元素后,只需将其与要查找的数字进行比较即可

当然,上面的代码有一个缺陷,在一我们通常不知道lower和upper(这两个值代表的是列表的下标)所有可以进行修改。

现在进行调用,看是否正确。

结果是正确的,但是你可能会好奇,明明对于列表,我们可以直接index查找,为什么要这样呢?

的确,index是可以的,但是效率太低了,前面说过,要在100个数字中找到指定的数字,只需问7次;但使用循环时,在最糟的情况下需要问100次。(大家看100好像没什么,事实上,在可观察到的宇宙中,包含的粒子数大约为10 87个。要找出其中的一个粒子,只需问大约290次!)

实际上,模块bisect提供了标准的二分查找实现。

相关推荐
明月看潮生4 分钟前
青少年编程与数学 02-003 Go语言网络编程 15课题、Go语言URL编程
开发语言·网络·青少年编程·golang·编程与数学
南宫理的日知录15 分钟前
99、Python并发编程:多线程的问题、临界资源以及同步机制
开发语言·python·学习·编程学习
coberup24 分钟前
django Forbidden (403)错误解决方法
python·django·403错误
逊嘘32 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
Half-up34 分钟前
C语言心型代码解析
c语言·开发语言
龙哥说跨境1 小时前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
Source.Liu1 小时前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust