抽象,自定义函数,递归

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提供了标准的二分查找实现。

相关推荐
数据智能老司机3 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机4 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机4 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机4 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i5 小时前
drf初步梳理
python·django
每日AI新事件5 小时前
python的异步函数
python
这里有鱼汤6 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook15 小时前
Manim实现脉冲闪烁特效
后端·python·动效
程序设计实验室15 小时前
2025年了,在 Django 之外,Python Web 框架还能怎么选?
python
倔强青铜三17 小时前
苦练Python第46天:文件写入与上下文管理器
人工智能·python·面试