1、函数介绍
函数(function),它首先是作为一个数学的术语先出现的,没记错的话在我国应该是初二数学的教学内容。函数在教科书中的描述如下:
给定一个数集A,假设其中的元素为x,对A中的元素x施加对应法则f,记作f(x),得到另一数集B,假设B中的元素为y,则y与x之间的等量关系可以用y = f(x)来表示,如下:
在数学中,函数是一种关系,它表达了一个输入值与一个输出值之间的对应关系,例如简单的一次函数、二次函数和三角函数,还有复杂点的指数函数、对数函数以及幂函数等。
对于计算机编程,函数的概念最初也源自于数学的函数,它也可以看作是一种数学函数的实现,因为它的目标也可以是根据输入参数执行相应的处理,然后输出期望的返回结果,也可以简单地用y = f(x)来表示。不一样的是,在编程中,函数的概念更加广泛一点,它更多的表示一组可重用的代码块,用于执行特定的任务。但它并没有规定一定要有输入和输出,它的目的除了包含实现类似于数学函数一样的 输入->激励->输出 功能外,更重要的是为了提高代码的复用性和可维护性。
例如,数学求圆面积的公式为:s = πr²,对于数学函数而言,它等价于s = f®,假如数集A中有2、3、4三个半径值,那么代入函数公式就得出数集B的值为π*2²、π*3²、π*4²。对于编程而言,对应代码如下:
r1 = 2
r2 = 3
r3 = 4
s1 = 3.14 * r1 * r1
s2 = 3.14 * r2 * r2
s3 = 3.14 * r3 * r3
如上,每求一个圆的面积都必须列一次完整的圆面积公式。虽然只有一行代码,读者可能感受不到它的麻烦,但如果是复杂点的功能,需要写多行代码才能实现的时候,你会发现同样的功能只是因为一个参数不一样就要拷贝多次来实现,这显然不是我们追求的效果。于是编程中使用函数来对这部分重复的功能进行封装。
对于求圆面积的函数而言,半径就是函数的输入参数,面积就是函数的输出参数或者返回值,例如:
def area(r):
return 3.14 * r * r
我们先不用管函数是怎么定义的,先看效果,定义函数后,代码变成了:
s1 = area(r1)
s2 = area(r2)
s3 = area(r3)
也就是说,area这个函数把求圆面积的实现细则给封装起来了,函数调用者不需要知道求圆面积的公式是什么,也不需要知道π值是啥,对于调用者来说,它相当于一个黑盒子,只需要知道代入半径,便可得出圆的面积值即可。求不同半径的圆面积,调用的都是同一个函数,只是传入的半径值不一样。函数降低了代码的复杂度,提高了代码的易读性或者可维护性。
晓晓在刚学C语言编程的时候,记得教科书上有这么一句话:“函数是C程序的基本单位”,Python说:
一个Python程序可以分解为多个功能独立的函数来实现,并且在Python程序设计中,我们通常将一些常用的功能模块封装成函数,放在公共函数库中供其他程序调用。
善于利用函数可以减少编写重复代码的工作量。同时,在多人协作开发的项目中,函数又可以作为各个模块之间交互的接口,使得项目开发能够并行进行。因此,学会定义和使用函数,是Python编程的必修课。
2、函数定义
在Python中,支持使用def关键字来定义一个函数,语法如下:
def 函数名(参数列表):
函数体
return 返回
函数名有如下规定:
-
函数名字必须以字母或下划线开头,不能是数字或者其他符号开头;
-
函数名字不能是Python保留的关键字,如if、else、for等;
-
函数名字中可以包含数字,但不能包含空格;
-
函数名字应具有描述性,避免使用单个字母或者非常用的英文缩写;
-
建议函数名尽量避免与标准库或者常用的第三方库重名,避免混淆。
函数定义需要注意:
-
函数定义后面有一个英文的冒号;
-
参数列表可以为空,但即使参数为空,括号也不能省掉;
-
函数体必须与def之间有一定的缩进,建议缩进4个空格;
-
函数可以没有return语句,没有return语句时函数默认返回None;
-
函数只要执行到return语句就会马上返回,后续代码直接跳过;
例如,前面的求圆面积的函数定义:
def area(r):
return 3.14 * r * r
area是函数名,r是参数,3.14*r*r是返回值,可见,返回值也可以是一个表达式。当然,你也可以这样写:
def area(r):
s = 3.14 * r * r
return s
这样写就多了一个变量赋值的过程,但是对于初学者来说可能更好理解一点。
Python支持定义一个空函数,用于函数打桩:
def nop():
pass
注意:定义空函数时,pass是必须要带的,它只是一个占位符,表示什么都不做,或者可以理解为我现在还没有想好怎么做,先pass,让代码能运转起来。此外,pass也可以放在其他语句里,如if else的执行语句、for while的循环体内等等,例如:
if a == b:
pass
像这种暂时没有任何处理的条件语句,它的执行语句也不能为空,如果不用pass临时填补,运行是会报语法错误的。
对于稍微复杂一点的函数定义,我们建议写上函数文档字符串。关于文档字符串的描述,我们在前面课程已经讲过,就是用一对三个双引号括起来的注释。在函数定义中,文档字符串可以用于描述函数功能、参数、返回值及异常信息等,目的是写给调用者或后续维护人员看的,帮助他们快速的了解并掌握函数的功能及使用方法。Python是没有规定函数文档字符串的具体格式的,允许编码者自行定义,一般格式如下:
"""
函数功能描述
Args:
参数:参数说明
Returns:
返回值:返回值说明
Raises:
异常:异常说明
"""
当然,你可以根据你所遵循的编程规,或者实际情况来编写合适的文档字符串。如果函数功能很简单,或者函数名字可以自解释的话,不写函数文档字符串也是完全没有问题的。
3、函数参数
和其他编程语言一样,我们定义Python函数的时候,一般会先把函数参数的个数、名字及位置确定下来,这种确定的参数在Python中又称为必选参数或者位置参数。Python的函数定义虽然简单,但是函数参数的灵活度却非常大,除了定义必选参数外,还支持使用默认参数、可变参数和关键字参数、命名关键字参数等。
定义函数时,函数括号内默认罗列的以逗号分隔的各个名称就是函数的位置参数,参数的命名和函数命名约束一样,格式如下:
def 函数名(参数1, 参数2, ……):
例如,上面的求圆面积函数:
def area(r):
return 3.14 * r * r
上面的参数r就是一个位置参数,当我们调用erea函数时,必须且只能传入一个参数,如:
area(2)
area(3)
如果没有传入参数,或者传入多于1个参数,都会导致运行异常,例如:
如果一个函数定义时有2个或者2个以上的位置参数,则调用时传入参数的顺序必须与函数定义的顺序一一对应,例如:
def circu(length, width):
return 2 * (length + width)
调用示例如下:
circu(3, 2)
其中3传递给对应位置的参数length,2传递给对应的位置参数width。
Python函数中的默认参数是一个可选的参数,它们在函数定义时提供一个默认值,如果在调用函数时没有提供该参数的值,则使用默认值,格式如下:
def 函数名(参数名=默认值):
例如:
def student(name, age, sex="male"):
pass
上面的name和age是位置参数,sex就是一个默认参数。因此,当你调用student函数时,如果没有传入sex参数,它就默认给sex参数赋值为“male”,例如:
# 默认参数sex没有赋值,赋默认值"male"
student("lilei", 18)
# 默认参数sex赋值为"female",把它传递给函数
student("hanmeimei", 17, "female")
默认参数的目的是为了简化函数的调用,即调用函数时,如果传递的参数与默认参数一致,则不需要显式地给默认参数赋值。
关于默认参数需要注意:
-
如果函数参数列表中同时存在位置参数和默认参数,那么位置参数必须在默认参数的前面(从左往右算),否则解释器会报错;
-
当函数中有多个默认参数时,可以按位置顺序提供默认参数,也可以不按顺序提供默认参数,当不按顺序提供默认参数时,必须带上参数名字。
例如有以下函数:
def student(name, age, sex="male", grade=5):
pass
可以按顺序传入默认参数:
student("hanmeimei", 17, "female", 6)
这时,"female"按位置关系是赋值给sex的,6则是赋值给grade的。
如果sex使用默认参数,grade不使用默认参数,怎么办呢?那就必须带上参数名,显式地给默认参数赋值了,如下:
student("hanmeimei", 17, grade=6)
此外,当带上参数名赋值时,可以不按默认参数的定义的顺序赋值,如:
student("hanmeimei", 17, grade=6, sex="female")
假如有这样一个需求:
定义一个函数,用它来计算不确定个数的多个数之和,也就是说:可以传三个数给函数,也可以传5个数给函数,函数需要根据实际传递的数来计算他们的总和。
当我们尝试定义这个函数时,或许读者会马上想到用列表或者元组来作为函数参数传入,然后在函数体内遍历列表或元组,计算所有元素的和,例如:
def mysum(nums):
sum = 0
for data in nums:
sum += data
return sum
这种方法固然也是可以实现的,但是必须在调用函数前,先组装出一个列表或元组来,如:
mysum([1, 2, 3])
mysum((1, 2, 3, 4, 5))
这样做也无可厚非,但是我们有更好的办法,那就是使用可变参数。
Python函数中的可变参数是指在函数调用时,可以接受任意数量的参数。可变参数的格式是在参数前加一个*号,格式如下:
def 函数名(*args):
例如:
def mysum (*args):
pass
这样就可以根据需要,传入不确定个数的参数了。当然,也可以是0个参数:
mysum()
mysum(1, 2, 3)
mysum(1, 2, 3, 4, 5)
函数可变参数接收到的是一个元组,因此上面的实现代码可以完全不变:
def mysum(*nums):
sum = 0
for data in nums:
sum += data
return sum
关于可变参数,再给读者介绍一个小知识,那就是当我们在列表或者元组变量前加一个*号时,表示获取列表或元组里的所有元素的值(有点C语言里取指针值的味道),一般用于作为函数参数时使用,相当于把元素展开传递给函数,一般传递给可变参数, 如:
nums = (1, 2, 3)
mysum(*nums) # 其效果相当于mysum(1, 2, 3)
但是,它并不是只能用于可变参数,你可以把*list或者*tuple理解为去掉列表或元组外壳的一组数据,他们可以放到任何展开后合法的使用场景中,例如:
nums = (1, 2, 3)
data = [2, 4, 6, *nums]
上述代码最终data的值为[2, 4, 6, 1, 2, 3],相信读者已经大概猜到它的原理了,可以简单理解为宏展开,其他使用场景请读者自行探索。
这是Python灵活的又一种表现了,不过灵活虽然好,但完全掌握是前提,把握不住的灵活不但不能给你带来好处,反而会遭其反噬哦。
Python中的关键字参数其实就是带参数名的可变参数,格式为参数前加两个\*号,关键参数的主要作用是用于扩展函数参数,格式如下:
def 函数名(**kwargs):
例如:
def student (**kwargs):
pass
关键字参数在函数内自动组装为一个字典,如:
def student(**kwargs):
print(kwargs)
student(name="lilei", age=18, sex="male")
上面代码把kwargs打印出来是一个字典:
可见,关键字参数的传参方式和默认参数一样,也是“变量名=变量值”,只是它可以传递任意个参数而已,当然,你也可以传递0个关键字参数,这样的话kwargs就是个空字典{}。
同理,我们也可以通过**符号来引用字典变量,并且把它作为关键字参数的入参,如:
data = {"name": "lilei", "age": 18, "sex": "male"}
def student(**kwargs):
print(kwargs)
student(**data)
这个输出和上一段代码输出是完全一样的。当然,**dict也一样可以用于任意展开后合法的使用场景,如:
base = {"name": "lilei", "age": 18, "sex": "male"}
data = {**base, "grade": 5, "class": 1}
print(data)
输出为:
命名关键字参数格式如下:
def 函数名(*, 参数1, 参数2, ….):
注意,命名关键字参数前有一个*号分隔符,*号后面的都视为命名关键字参数,例如:
def student(*, name, sex):
pass
上面的name和sex都是命名关键字参数,在调用函数的时候,必须带上参数名,如:
student(name="lilei", sex="male")
且传参必须与定义的命称及个参数个数一致,但顺序不要求一致,例如,你可以这样传参:
student(sex="male", name="lilei")
关于命名关键字参数,刚开始学时因为它的名字原因,尝试从关键字参数的角度去理解它,发现并不好理解,因为它并不是一个可变参数,而关键字参数本质上是一个可变参数,因此:
命名关键字参数 ≠ 命名 + 关键字参数
反而,你可以简单的把它理解成命名的位置参数。为什么呢?因为位置参数是强制按顺序传参,而命名关键字参数是强制用命名来传参。此外,命名关键字参数也有点默认参数的味道,因为也可以指定默认值,指定了默认值的命名关键字参数可以不传参,如:
def student(*, name, sex="male"):
print(name, sex)
student(name="lilei")
上述例子中,因为命名关键字参数sex指定了默认值,函数调用的时候,如果传参与默认值一致,可以不传参。
如果命名关键字参数前面已经有一个可变参数,则不能再带*号分隔符,默认可变参数之后的都是命名关键字参数,如:
def student(*args, name, sex):
print(args, name, sex)
但下面代码是错误的:
def student(*args, *, name, sex):
print(args, name, sex)
该代码运行会抛出异常。
上述是Python函数中支持的5种参数类型,Python函数定义时允许组合使用它们,称为混合参数。使用混合参数时,需要注意,参数定义的顺序必须是:
(位置, 默认, 可变, 命名关键字, 关键字)
举一个支持全部类型参数混合的例子:
def student(name, age, sex="male", *hobby, father, mother, **score):
pass
这里name和age是位置参数,sex是默认参数,hobby是可变参数,father和mother是命名关键字参数,score是关键字参数。一个合法的函数调用如下:
student("lilei", 18, "male", "dance", "sing", father="lisi", mother="zhangsan", math=100, English=99, chinese=70)
需要注意的是:当参数列表中同时存在默认参数和可变参数时,调用函数时默认参数必须赋值,且必须按位置参数方式赋值,即上面的第三个参数赋值不能以sex="male"这种方式赋值。当然,如果没有可变参数,则无此限制,即默认参数可以不赋值也可以“名=值”方式赋值,请读者自行尝试。
4、函数返回值
在上面 9.2 函数定义 中已经介绍,Python函数可以通过return语句结束函数,并返回一个值。函数的return语句可以返回任何类型的数据,包括数字、字符串、列表、元组、字典等,如上面的求圆面积的例子:
def area(r):
return 3.14 * r * r
它返回的就是一个浮点类型的数值。
其他返回值类型这里就不举例了,请读者自行尝试。如果函数结束时没有调用return语句,则默认返回None。
python函数的返回值可以直接赋值给变量,当然,也可以直接把函数A作为函数B的参数使用,相当于会先运行函数A,把函数A运行的返回值赋值给B对应位置的参数,例如:
# 求圆的面积
def area(r):
return 3.14 * r * r
# 返回两个数中较大的一个
def mymax(s1, s2):
return s1 if s1 >= s2 else s2
# 求两个圆面积较大的一个
s = mymax(area(2), area(3))
5、函数调用
上面已经举了大量函数定义和调用的例子,相信读者已经大概知道函数调用的方法和注意事项了,这里主要是想补充说明两点:一个是函数调用时传参的数据类型问题,另一个是关于值传递还是引用传递的问题。
- 参数数据类型:
Python的函数可以接受任意类型的参数,包括基本数据类型(整型、浮点型、字符串等)和引用数据类型(列表、元组、字典等)。还记得我们前面 第5课 Python数据类型 中对数据类型分类的相关描述吗?
对于像C语言一样的静态类型语言,函数定义时必须指定参数的数据类型,函数调用时传递的参数数据也必须与函数参数的数据类型一致,否则编译过程就会报错!但是对于Python这样的动态类型语言,通过上面的学习读者应该也知道了,它们的函数定义时并不需要指定数据类型,这样的话就会存在函数调用时传参数据类型不匹配的问题,例如:
# 求两个数的最大值
def mymax(num1, num2):
return num1 if num1 >= num2 else num2
该函数定义时并不能规定num1和num2的数据类型,当调用该函数时,如果正常传入两个数字类型的数值,则运行是正常的,如:
a = mymax(2.3, 3.4)
输出是3.4。
但是,如果函数调用者故意传入非数字类型的数据,将会导致该函数运行时出现TypeError异常, 例如:
a = mymax("2", 3)
运行异常如下:
很明显,出错的原因是因为字符串和整型之间不能用>=关系运算符。
为了避免出现这种情况,函数设计的时候可以加上参数类型的判断或者处理。例如,可以在函数入口尝试把字符串类型转换为数字类型,如果转换不了就打印提示或者返回错误码。当然这只是一种建议,实际上此类型的错误我们还是希望通过raise异常的方式来呈现比较好。
关于异常,我们将在后续的 进阶篇 相关课程中详细讲解。
- 值传递与引用传递:
如果读者有学过C语言,应该知道有一个东西叫做指针,也知道C语言的函数参数有传值和传指针这两种方式。没学过C语言也没关系,我们现在来好好盘一盘。首先,Python没有指针的概念,读者也不用管什么是指针,我们先来看两个Python函数的例子,第一个:
def func(var):
var = 10
print("var :", var)
data = 5
func(data)
print("data:", data)
代码运行的结果是:
此例子中,func函数尝试对参数var的值进行修改,函数内var值修改成功了,但是并没有影响它的输入参数,即整型变量data的值。
第二个例子:
def func(var):
var[0] = 3
print("var :", var)
data = [1, 2]
func(data)
print("data:", data)
此例子中,func函数尝试对参数var的值进行修改,函数内var值修改成功了,并且也影响了它的输入参数,即列表变量data的值。
对比这两个例子,先思考一下区别点在哪里。
为了弄清楚这个问题,我们需要先复习一下 第5课 Python数据类型 中关于Python变量定义与赋值的相关知识。对于函数参数的赋值,其原理与普通变量赋值是一样的,对于上面第一个例子,代码过程图解如下:
由于这种参数传递方式效果等同于只把值复制给了函数参数,而不是把变量的地址传递过去,所以一般我们称之为值传递方式。
而第二个例子的传入参数是一个列表,和我们之前学过的引用类或容器类变量的赋值原理是一样的,代码过程图解如下:
由于这种参数传递方式效果等同于把变量的地址传递给了函数参数,而不是把值复制过去,所以称之为引用传递或地址传递方式。
已经很明白了,对于不可变数据类型的参数,都是值传递,对于引用类型或者容器型的参数,都是引用传递。但请读者注意,这里并不是说引用传递就一定能修改变量的值哦,那也得变量本身是可写的数据类型才行,比如元组,虽然它也是引用数据类型,但是它是只读的呀!
6、函数式编程
对于复杂软件或系统的开发,我们一般会把软件功能分解为一个个简单的任务,用函数方式来实现这些任务,然后再把这些函数统筹管理起来就能实现整体软件的功能了,这是软件项目中的一个很常见的做法,称为函数式编程。函数式编程是一个很重要的概念,理解起来简单,但是实践起来也不容易。
笔者征战码场十多年,见过许多工作了十年八年都还没领悟的人,他们要么完全没有函数式编程的思维,一个几千行代码的软件,他们能把所有功能放一起从头写到尾;要么就有一点函数式编程的想法,但是又不会准确的划分函数功能范围,认为只要把一堆代码捆在一起就是函数,他们自己也说不清楚这个函数是实现什么功能,而且思维大多已经定型,甚至还自我感觉挺良好,我称这类人为码林霸(BUG)主,读他们代码的人都是码林盟主(蒙住),他们简直是项目的杀手、测试团队的恩人!
为了让读者能更好的理解函数式编程,笔者决定糊弄一个通俗点的例子。
大家应该都去下过馆子吧,当然,这里是指规模较大一点的餐馆,他们一般设有以下工种:采购、厨房、服务、财务、清洁。餐馆老板一般会根据工种来设置岗位,并明确各个岗位的工作内容。笔者把每个工种比喻成一个功能函数,并明确函数的功能以及输入输出,如下:
|
各个“函数”各司其职,每个“函数”只需要做好自己的本职工作就行了,不要去考虑别的事情。例如,搞采购的你只需要从财务那里拿钱去按菜单买回来食材就行了,不要去考虑厨房是怎么烹煮的,那是厨房的职责;厨房也只需要负责把饭菜做美味就行了,不要去想煮好的饭菜会不会端错地方,那是服务要做的事情。
这些专职的“函数”之间还需要一个统筹的管理者,那就是总经理或者老板,他是“函数调用者”,负责策划并指挥好什么岗位什么时候做什么事情,如果没有他,各个“函数”之间可能无法协调工作,例如,需要先安排采购把菜买回来,才能让厨房去烹煮;另外,清洁得把盘子清洗干净交给厨房,厨房才能把饭菜盛进去,然后才能让服务端出去给客人,总不能直接整锅端出去给人家吃吧?
当然,不管是餐馆的运营还是软件系统的运行,远没有这么简单,例如,还有反馈机制、“函数”之间的互相沟通协调机制、突发或者异常处理机制等等,这里就先不发散讲解了。
最后,关于Python函数,还有一些高级用法,比如高阶函数、返回函数、匿名函数、函数装饰器、偏函数等,这些我们把它们放到后续 进阶篇 相关课程中讲解,本文的重点是先快速的教会大家如何定义和使用函数。
至此,任务已完成。
pCeTo9O47L9XV5Ew),如有侵权,请联系删除。
感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
工具都帮大家整理好了,安装就可直接上手!
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。
四、Python视频合集
观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
五、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
六、面试宝典
简历模板
若有侵权,请联系删除