自定义函数
- 我们在之前的学习中,已经简单的定义或调用过一些函数,函数简单来说,就是将一些语句打包起来,方便我们再次使用
- 比如 python 内置的一些函数,如选出最大值的
max函数 - Max 函数在给定序列中找出最大的那个并返回,如果我们要实现这个功能,可能需要多行语句
- 如果有一个序列需要进行这样的操作,写一遍这样的语句也没什么,但如果有几十个几百个这样的序列需要进行同样的操作呢?这时我们就需要用到函数
- 我们可以尝试将上述写的语句打包成一个找出最大值的函数,如果需要用到这些语句,我们只需要进行简单的调用即可
- 定义函数要用
def关键字,后面是函数名,括号内代表函数的形式参数 - 所谓形式参数,就是在定义时赋予函数,并可以在函数内进行调用的参数
- 我们可以定义一个简单的函数尝试一下
- 将变量
name传入函数进行调用 - 有些人可能会感到奇怪,我们定义函数时,接受的形式参数,一个是位置参数
my_name,另一个是默认值参数my_nickname,可传入的变量不是这两个中的任意一个,而是名为name的变量,为什么函数还能继续运行呢? - 如果我们将 name 这个变量改名为
my_name再传入函数中,函数还能运行吗? - 要知道这些问题的答案,我们首先要明白作用域这个概念
- 我们刚才定义的函数
my_introduction会开辟一个新的作用域,这个作用域里面存放了各种变量,函数在调用变量时,会现在自己开辟的作用域中寻找,如果找不到再去全局作用域中寻找。就像是村里有个人四处奔走在找一个叫狗蛋的人,我们肯定会觉得他是在找本村的叫狗蛋的人。如果你是这个村的村支书,知道这个村里没有叫狗蛋的,你就会上前告诉他这里没有叫狗蛋的,要去其他地方找找 - 当
my_introduction被调用时,他会返回f"在外面叫{my_name},在里面叫{my_nickname}"这个字符串,由于这个字符串中需要用到my_name,my_nickname,这两个变量,所以函数会去寻找这两个变量 - 函数接受了
name变量,实际是将name变量的值赋值给了my_name,而my_nickname的值在函数的作用域中本身就已经赋给过了,所以函数能正常找到变量的值成功运行 - 像上面的
my_nickname这样的形式参数,叫做默认值参数,我们在定义时给了该变量一个默认的值,如果不在函数调用的进行修改,变量将会引用默认值 - 介绍了基本的函数定义和形式参数后,我们来学习一下函数的完整定义
- 函数的完整定义由下图所示
- 仅限位置参数只能通过按位也就是按顺序传参的方式传值,仅限关键字参数只能通过关键字的方式传值
- 当我们不使用
/和*对参数进行限制时,我们的参数,既可以通过位置传入,也可以通过关键字的方式传入 - 也就是说,当我们不对形参做任何特殊限制时,各个形参之间没有任何区别
- 所谓的位置参数和关键字参数,只是按传入值的方式给参数归类而已
- 我们定义一个简单的函数,来看下位置或关键字参数,也就是不受任何限制的参数的实际应用
- 位置或关键字参数,既能通过顺序传值,也能通过关键字传值,甚至能混合传值,通过位置传值时,该参数就是位置参数,通过关键字传值时,该参数就是关键字参数
- 要注意,在参数没有默认值的情况下,形式参数和实际参数的个数一定要对应
- 少传了值后会报错
- 参数也能和变量一样,通过解包的方式进行传值,接收一个序列
- 解包传值时,一定要注意实际传入的参数数量和形参数量是否相等
- 否则会报错
- 也可以用解包字典的方式,来进行特殊方式的关键字传值
- 在利用解包字典传值时,我们要注意,字典的键和形参的名字一定要对应,顺序则可以随意打乱,否则会报错
- 报错信息显示意料之外的关键字参数
- 当我们将给形参赋一个默认值时,这个参数就变成了默认值参数,默认值参数最大的特点时像字典一样
K = V的形式在定义时就有了默认值 - 同样,默认值参数也是位置或关键字参数,可以按位或者按关键字方式传入参数
- 结果
- 从调用和结果看来,默认值参数并没有太多不同,都可以通过按位置传入和按关键字传入的方式来传值,最大的区别,是默认值参数有默认值,可以不传值,以默认值使用,我们之后用到的很多模块内或内置的函数,都具有默认值参数
- 要注意的是,默认值参数在没有特殊需求的时候,尽量使用不可变类型的对象,原因我们在列表那一节的深浅拷贝已经讲过
- 大家在进行各种尝试的过程中,尤其是用混合方式传入值的时候,可能会发生报错,例如我们先用关键字的形式给第一个参数赋值,然后再按位置给其他参数赋值
- 我们已经可以看到,vscode 自动在 200 和 300 下面标记了波浪线,这就代表这部分是有问题的
- 我们运行程序,果然报错
- 报错提示,位置参数必须要在关键字参数之后,这是为什么呢?
- 这是因为,python 函数的赋值逻辑,是优先按照位置顺序赋值,再按照关键字来赋值的
- 我们以为的赋值顺序
- 实际上的赋值
- 如果关键字参数在位置参数之前,赋值就会像上述的流程一样出现重复赋值,为了避免出现这种歧义,python 就规定了位置参数必须要在关键字之前。注意:这个规则不仅要在实际传值的时候遵守,在定义时也要遵守
- 位置参数能用解包的方式,接收一个序列,那么,关键字参数也同样可以通过解包的方式,接收一个序列,甚至接收一个字典
- 运行结果
- 在对不限制的参数有一定了解后,我们来看一下函数定义中的两个特殊符号
/和* /用来限制形式参数,在/前面的形式参数只能是位置参数,只能通过按位传值的方式传入参数,/后面的是位置或关键字参数- 正常调用没有问题,但如果对形式参数
a1和a2用关键字的方式传入参数的话程序则会报错 - 报错结果如下
- 和
/对应的*则是仅限关键字参数,在*前的是位置或关键字参数,之后的则是仅限关键字参数 - 同样,对于仅限关键字参数,不能通过按位传值的方式传入
- 通过上面的学习,我们发现,对函数的参数进行定义时,最重要的就是该参数的==传值方式==
- 传值的方式大体分为三种:==按位置传值(解包序列),按关键字传值(解包字典)以及默认值传入==
- 直到现在,我们学习的都是像
a这样的普通参数,接下来我们来学习两个特殊的形参*a和**a - 普通的形参,例如
a、b、c,可以接收任意类型的对象,就像这样,既可以接收数字,也可以接收字典和列表 - 既然普通的参数功能这么齐全,那么为什么还要在参数前面加一个或两个
*号呢?这个特殊符号的功能是什么呢? - 注意,这是一个非常关键的问题,形参
a和形参*a和形参**a到底有什么本质上的区别? - 我们定义三个函数,形参分别是
a、*a和**a - 首先我们来看
a和*a在接收序列时的差异 - 我们传入不同形式的序列
- 大家会发现,和想象中并不一样,形参
a在接收时,只能接收单个值,或者单个序列,并不能直接接收多个值。而*a可以接收无限个值,也就是说*a接收的是无限个传入值组成的序列 - 那
a和**a的区别也在于此,a接收的是单个字典,而**a接收的是无限个键值对模式传入的值
函数文档
- 到现在,我们已经学会了函数的基本定义和传参,接下来,我们学一个和编程技术没有太大关系但仍然非常重要的东西——函数文档
- 我们在 VSCode 中,写一个普通的“hello world”语句,然后将鼠标移到
print上 - 我们会发现在鼠标的位置突然出现了一大堆东西,这就是函数的说明文档
- 其中,
*value、sep、end、file、flush都是我们之前学到过的参数 *value用来接收无限个传入值组成的序列,例如我们传入的"hello world!"这个字符串sep接收字符串类型的对象,None = " "代表sep的默认值是" "end和sep一样,接收字符串类型的对象,默认值是"\n"file是支持写入的字符类型,默认为空flush接收一个布尔值,默认为False- 我们向下滚动,查看更多说明
- 我们可以从说明中看到,
sep参数的作用是给所有输入值增加分割符,默认是空格 - 我们可以试一下
- 再看一下
end,它的值代表输出完所有值后的结束符,默认是"\n",我们在之前字符串那节的转义字符中学过,是换行的意思,我们也可以尝试一下,把end的值换成其他 - 通常将
end参数改成""来取消换行 - 接下来是
file参数,可以将原本打印到终端的值写入到文本中 - 结果如下
- 最后是
flush参数,用来决定是否立即刷新缓存区,一般不推荐使用(暂时还不明白实践中 True 和 False 之间的区别) - 以上是 python 内置的 print 函数所自带的文档,现在,我们要给自己写的函数写一份文档
- 我们写一个非常简单的加法函数,将两个参数相加
- 函数中第一个字符串就是函数的注释,也就是说明文档
- 为了方便和简洁,我们用多行字符串来书写说明文档
- 字符串内最开始可以简单地写一下函数的作用是什么,用中文也可以,最好用英文
Args标题后,用来对各个形参进行说明Return标题后跟着这个函数的返回值Example标题后跟着以交互模式表现出的实例- 函数的说明文档不需要多高大上,最重要的是告诉使用这个函数的人,你的函数以及各个参数的作用是什么
- 说明文档和注释一样,虽然不起眼但非常重要,尤其是大型项目中
- 如果要进阶说明参数的类型和返回值的类型,可以在参数后面加
:
Lambda函数
- 最后我们来介绍一下 lambda 函数
- Lambda 函数是一个非常简单的轻量化函数,我们可以用 lambda 快速创建一个返回两数相加的值的函数
- Lambda 函数和普通函数一样,接收参数并返回值,我们可以把它当作一个一句话函数