第 6 章 函数
在前面几个章节中我们经常使用到print(),那么它是什么呢?
print() 是一个函数,可以向控制台打印输出内容。
6.1 函数的概念
函数是带名字的代码块,用于完成具体的任务,可重复使用。当需要在程序中多次执行同一项任务时,无须反复编写完成该任务的代码,只需要调用执行该任务的函数,让Python运行其中的代码即可。
通过使用函数,程序编写、阅读、测试和修复起来都更加容易。Python中的函数必须先定义后使用,Python提供了许多内建函数,比如print()。也可以自己创建函数,这被叫做用户自定义函数。
案例:在控制台打印输出一个2x3的*,那么可以编写如下代码
‘’’
该案例演示了向控制台打印23的 ""
‘’’
row = 2
while row > 0 :
print(“" * 3)
row -= 1
如果:我现在想要再次输出这样的图形,那么以我们现在的知识,我们的代码就会很冗余。那么这个时候就可以通过定义函数来解决我们的问题。
row = 2
while row > 0 :
print("” * 3)
row -= 1
print(“-” * 50)
如果需要再次输出
row = 2
while row > 0 :
print("" * 3)
row -= 1
6.2 函数的定义
6.2.1 语法
Python 定义函数使用 def 关键字,一般格式如下:
def 函数名 (参数列表) :
函数体
[return]
6.2.2 定义一个函数的规则
函数代码块以def关键词开头,后接函数标识符名称和圆括号 ()。
任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。用三个引号括起来,单引号和双引号都可以。
函数参数后面以冒号结束。
函数体开始缩进。
return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
6.2.3 函数名
函数名是程序员给这个函数起的名称,需要遵循标识符的命名规则。
函数名一般是一个动词,第一个单词小写,其他每个单词的首字母大写。
6.2.4 参数
函数在完成某个功能时,可能需要一些数据,在定义函数时指定函数参数来接收这些数据。如在屏幕上打印信息,需要把要打印的信息传递给print()函数。如果有多个参数,参数之间使用逗号分隔。函数也可以没有参数,但是小括弧不能省略。
6.2.5 函数体
调用函数时执行的代码,可包含函数说明文档与返回值。
6.2.6 返回值
有些函数在执行的时候,需要有返回值给调用者,通过return关键字进行返回,返回到函数调用的位置。
6.3 函数的抽取以及调用
在上面打印两次2x3的的案例中,我们的代码出现了冗余。我们分析,可以将打印2x3的这个功能封装为一个单独的函数。
函数定义好之后,通过函数名()对函数进行调用。
(1)案例:打印两次2x3的。
‘’’
该案例演示了函数的抽取以及调用
打印如下图形
***
***
-------
***
***
‘’’
定义一个函数,该函数完成打印输出23 ""的功能
def printStar() :
‘’’
这是对函数功能的说明
‘’’
row = 2
while row > 0 :
print(“*” * 3)
row -= 1
调用函数
printStar()
print(“-” * 20)
printStar()
(2)注意:
函数必须先定义再调用
函数在定义的时候只是告诉解释器我定义了一个这样的函数,可以完成某些功能,但是这个时候函数还没有执行,需要调用函数后,才会执行。
6.4 使用函数的好处
使程序变得更简短而清晰
可以提高程序开发的效率
提高了代码的重用性
便于程序分工协作开发
便于代码的集中管理
有利于程序维护
6.5 函数的参数
6.5.1 参数的抽取
在上面的案例中,虽然我们抽取了函数去完成打印2x3的*功能。如果现在希望打印出如下图形,该如何实现?
1)第一种方式:不封装函数
‘’’
该案例演示了不封装函数
向命令窗口打印输出
***
***
--------
****
‘’’
row = 2
while row > 0 :
print(“*” * 3)
row -= 1
print(“-” * 20)
row = 1
while row > 0 :
print(“" * 4)
row -= 1
2)第二种方式:封装函数
‘’’
该案例演示了封装函数
向命令窗口打印输出
***
***
--------
****
‘’’
def printStar1() :
‘’’
该函数可以打印2行3列的
‘’’
row = 2
while row > 0 :
print(”*" * 3)
row -= 1
def printStar2() :
‘’’
该函数可以打印1行4列
‘’’
row = 1
while row > 0 :
print(“*” * 4)
row -= 1
调用函数
printStar1()
print(“-” * 20)
printStar2()
注意:这里我们虽然封装了函数,但是printStar1和printStar2这两个函数中的大部分代码还是一样的,存在冗余。
我们从上图可以看出来,printStar1和printStar2中不一样的地方就是在函数体中row变量值和打印的个数不一样。这两个值分别可以理解代表打印的行数以及打印的列数,既然我们现在的需求,打印的行和列是不固定的,所以我们只提供打印的函数,具体要打印几行几列的,让函数的调用者自己决定。这就要求我们在提供函数的时候,需要通过函数的参数接收行和列数据(函数的形式参数-形参);在调用函数的时候需要把行和列数据作为参数传递过来(函数的实际参数-实参)。
3)第三种方式:封装带参数的函数
‘’’
该案例演示了封装带参数的函数
向命令窗口打印输出
***
***
--------
****
‘’’
def printStar(row,col) :
while row > 0 :
print(“*” * col)
row -= 1
printStar(2,3)
print(“-” * 20)
printStar(1,4)
6.5.2 形参和实参
在定义函数时,指定的参数称为形式参数,简称为形参(函数的提供者)
在调用函数时,给函数传递的参数称为实际参数,简称为实参(函数的调用者)
在定义函数时,形参没有分配存储空间,也没有值,相当于一个占位符;
在调用函数时, 会在栈区中给函数分配存储空间, 然后给形参/局部变量分配存储空间,传递的是实际的数据
当函数执行结束,函数所占的栈空间会被释放,函数的形参/局部变量也会被释放
6.5.3 函数的参数传递
1)在 python 中,类型属于对象,变量是没有类型的:
a=10
a=“helloworld”
以上代码中,10是数字类型," helloworld " 是 String 类型,而变量 a 是没有类型,她仅仅是一个对象的引用(一个指针),可以是指向数字类型对象,也可以是指向 String 类型对象。
2)引用的概念
在 Python 中,变量和数据是分开存储的,数据保存在内存中的一个位置,变量中保存着数据在内存中的地址,变量中记录数据的地址,就叫做引用。
使用id()函数可以查看变量中保存数据所在的内存地址
注意:如果变量已经被定义,当给一个变量赋值的时候,本质上是修改了数据的引用,变量不再对之前的数据引用,变量改为对新赋值的数据引用,变量的名字类似于便签纸贴在数据上。
3)可变(mutable)与不可变(immutable)类型对象
在Python常见的类型中,数字类型、string、tuple和number是不可更改的对象,而list、set、dict等则是可以修改的对象。
不可变类型
变量赋值 a=500 后再赋值 a=1000,这里实际是新生成一个 int 值对象 1000,再让 a 指向它,而500被丢弃,不是改变a的值,相当于新生成了a。
可变类型
变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。
4)Python函数的参数传递
不可变类型
类似c++的值传递,如整数、字符串、元组。如fun(a),传递的只是a的值,没有影响a对象本身。比如在fun(a)内部修改a的值,只是修改另一个复制的对象,不会影响 a 本身。
可变类型
类似c++的引用传递,如列表,字典。如 fun(la),则是将 la 真正的传过去,修改后fun外部的la也会受影响
Python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。
案例: Python函数传不可变对象实例
‘’’
该案例演示了Python函数传递不可变对象
‘’’
def changeInt(a) :
print(“函数体中未改变前a的内存地址”,id(a))
a = 10
print(“函数体中改变后a的内存地址”,id(a))
b = 2
changeInt(b)
print(b)
print(“函数外b的内存地址”,id(b))
输出结果:
函数体中未改变前a的内存地址 140711474555352
函数体中改变后a的内存地址 140711474555608
2
函数外b的内存地址 140711474555352
id()查看对象的内存地址
说明:实例中有 int 对象 2,指向它的变量是b,在传递给 changeInt 函数时,按传值的方式复制了变量 b,a 和 b 都指向了同一个 int 对象,函数外b的内存地址和未改变前a的地址是相同的。在 a=10 时,则新生成一个 int 值对象 10,并让 a 指向它。这个时候内存地址也发生了改变。
案例:Python函数传可变对象实例
‘’’
该案例演示了Python函数传递可变对象
‘’’
def changeList(myList) :
myList[1] = 50
print(“函数内的值”,myList)
print(“函数内列表的内存”,id(myList))
mlist = [1,2,3]
changeList(mlist)
print(“函数外的值”,mlist)
print(“函数外列表的内存”,id(mlist))
输出结果:
函数内的值 [1, 50, 3]
函数内列表的内存 1546427570560
函数外的值 [1, 50, 3]
函数外列表的内存 1546427570560
说明:
可变对象在函数里修改了参数,那么在函数外面,这个原始的参数也被改变了。
通过内存地址的输出,我们可以看出来,是在原有的列表对象上进行的修改。
5)var1 *= 2与var1 = var1 * 2的区别:
var1 *= 2使用原地址。
var1 = var1 * 2开辟了新的空间。
同样的对于类似,var1 +=2 和 var1 = var1 + 2也是同理
def multiply2(var1):
print(“函数内var1 id:”, id(var1))
var1 *= 2
print(“var1 *= 2后,函数内var1 id:”, id(var1))
var1 = var1 * 2
print(“var1 = var1 * 2后,函数内var1 id:”, id(var1))
list1 = [1, 2, 3]
print(“list1 id:”, id(list1))
multiply2(list1)
输出结果:
list1 id: 2302584035712
函数内var1 id: 2302584035712
var1 *= 2后,函数内var1 id: 2302584035712
var1 = var1 * 2后,函数内var1 id: 2302584033664
6.5.4 函数可使用的参数形式
1)必须参数
调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于位置把每个相应位置的实参和形参相关联,调用时的数量必须和声明时的一样。
‘’’
该案例演示了函数位置实参
‘’’
def func(a, b, c):
print(a, b, c)
func(1, 2, 3) # 1 2 3
可以看到,1传给了a,2传给了b,3传给了c。
2)关键字参数
函数调用使用关键字参数来确定每个变量传入的参数值,使用关键字参数允许函数调用时参数的顺序与声明时不一致。
‘’’
该案例演示了函数调用时的关键字参数
‘’’
def printInfo(name,age) :
print(“姓名:”,name)
print(“年龄:”,age)
Python解释器可以通过age和name这样的关键字去和形参进行匹配
printInfo(name = “zhangsan”,age = 18)
printInfo(age = 18,name = “zhangsan”)
3)默认值参数
定义函数时,可给每个形参指定默认值。在调用函数时,给形参提供了实参则使用指定的实参值,否则使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。
‘’’
该案例演示了函数调用时的默认参数
‘’’
def printInfo(name,age = 20) :
print(“姓名:”,name)
print(“年龄:”,age)
printInfo(“zhangsan”)
printInfo(“lisi”,30)
printInfo(age = 40,name = “wangwu”)
4)不定长参数
参数的个数是不确定的。
(1)语法:
def 函数名([普通参数,] *var_args_tuple ):
函数体
(2)案例:
‘’’
该案例演示了函数调用时的不定长参数
‘’’
def printInfo(num,*vartuple):
print(num)
print(vartuple)
printInfo(70,60,50)
print(“-” * 20)
如果不定长的参数后面还有参数,必须通过关键字参数传参
def printInfo1(num1,*vartuple,num) :
print(num)
print(num1)
print(vartuple)
printInfo1(10,20,num = 40)
print(“-” * 20)
如果没有给不定长的参数传参,那么得到的是空元组
printInfo1(70,num = 60)
(3)注意:
加了星号 * 的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。
如果形参中出现了不定长参数,那么在调用函数的时候,先通过位置进行必须参数的匹配,然后不定长参数后面的参数必须通过关键字参数匹配
如果不定长的参数后面还有参数,必须通过关键字参数传参
还有一种就是参数带两个星号 **的可变长参数,基本语法如下:
def 函数名([普通参数,] **var_args_dict ):
函数体
加了两个星号 ** 的参数会以字典的形式导入,后面就不能再有其他参数了
‘’’
该案例演示了函数调用时的不定长参数
‘’’
def printInfo(num,**vardict):
print(num)
print(vardict)
# return
printInfo(10,key1 = 20,key2 = 30)
printInfo(10,a = 20,b = 30)
6.5.5 解包传参
若函数的形参是定长参数,可以通过 * 和 ** 对列表、元组、字典等解包传参。
def func(a, b, c):
return a + b + c
tuple11 = (1, 2, 3)
print(func(*tuple11))
字典中key的名称和参数名必须一致
dict1 = {“a”: 1, “b”: 2, “c”: 3}
print(func(*dict1))
6.5.6 强制使用位置参数或关键字参数
/ 前的参数必须使用位置传参, 后的参数必须用关键字传参。
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
f(1, 2, 3, d=4, e=5, f=6)
6.5.7 防止函数修改列表
有时要函数对列表进行处理,又不希望函数修改原列表,可以使用 copy.deepcopy()。
import copy
def multiply2(var1):
var1[3].append(400)
print(“函数内处理后:”, var1)
list1 = [1, 2, 3, [100, 200, 300]]
print(“函数外处理前:”, list1)
multiply2(copy.deepcopy(list1))
print(“函数外处理后:”, list1)
6.6 函数说明文档
编写了函数说明文档后,可以通过 help(函数名) 获取函数说明文档。
def adult(age=18):
“”“根据年龄判断是否成年”“”
result = “未成年”[age >= 18 :]
return result
help(adult)
PyCharm中将鼠标悬停在函数名上方也可以看到函数说明文档。
6.7 返回值
在程序开发中,有时候希望一个函数执行结束后,告诉调用者一个结果,以便调用者针对具体的结果做后续的处理。返回值就是函数完成工作后,给调用者的一个结果
在函数中使用 return 关键字可以返回结果 ,并结束正在执行的函数
如果return后面跟[表达式],在结束函数的同时向调用方返回一个表达式。
如果仅仅是return关键字,后面没有加内容,函数执行返回调用方None。
调用函数一方,可以使用变量来接收函数的返回结果
(1)不带表达式的 return 语句,返回 None。
def f(a, b, c):
pass
return
print(f(1, 2, 3)) # None
(2)函数中如果没有 return 语句,在函数运行结束后也会返回 None。
def f(a, b, c):
pass
print(f(1, 2, 3)) # None
(3)用变量接收返回结果
def add(num1,num2) :
‘’‘求两个数的和’‘’
sum1 = num1 + num2
return sum1
res = add(10,20)
print(“两个数的和为:” ,res)
(4)return 语句可以返回多个值,多个值会放在一个元组中。
def f(a, b, c):
return a, b, c, [a, b, c]
print(f(1, 2, 3)) # (1, 2, 3, [1, 2, 3])