在中学数学中我们知道 y=f(x) 代表着函数,x 是自变量,y 是函数 f(x) 的值,给定 x 可以计算出对应的 y。在程序设计中,函数的功能是一样的,给定输入,返回对应的输结果,变量 x 不在限制为数字,可以为任意的数据类型,比如字符串,列表,字典,对象,或者自定义的对象等,同样地返回值也可以任意的数据类型。函数的作用是对加工细节的一种封装,对外提供统一的接口,使用者无需关心函数对内的细节,是最基本的一种代码抽象方式。
函数不仅减少代码行数,而且能节省内存,提高程序运行速度:当一个函数调用完毕时,退出程序堆栈,内存空间被回收,当新的函数被调用时,局部变量又可以重新使用相同的地址。当一块数据被反复读写,其数据会留在 CPU 的一级缓存中,访问速度非常快,从而加快程序执行速度。
下面来说一说 Python 中的函数。
定义一个函数
Python 定义函数的规则:
-
函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
-
任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
-
函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
-
函数内容以冒号起始,并且缩进。
-
return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
使用 def 关键字,一般格式如下:
def 函数名(参数列表):
函数体
以简单的数据计算函数为例,定义函数 fun(a,b,h) 来计算上底为 a,下底为 b,高为 h 的梯形的面积:
>>> def fun(a,b,h): #def 定义函数fun,参数为a,b,h
... s=(a+b)*h/2 #使用梯形的面积计算公式,注意此行前有4个空格
... return s #返回面积,注意此行前有4个空格
...
>>> fun(3,4,5) #计算上底为3,下底为4,高为5的梯形的面积
17.5
将常用的处理过程封装成函数,在需要的时候调用它,可以屏蔽实现细节,减少代码数量,增强程序可读性。假如有许多个梯形的面积需要计算,实例如下:
>>> for a,b,h in [(3,4,5),(7,5,9),(12,45,20),(12,14,8),(12,5,8)]: #计算5个梯形面积
... print(\"上底{},下底{},高{}的梯形,面积为{}\".format(a,b,h,fun(a,b,h))) #字符串格式化函数format
...
上底3,下底4,高5的梯形,面积为17.5
上底7,下底5,高9的梯形,面积为54.0
上底12,下底45,高20的梯形,面积为570.0
上底12,下底14,高8的梯形,面积为104.0
上底12,下底5,高8的梯形,面积为68.0
普通函数
上例中的调用方法fun(3,4,5)并不直观,为了增加可读性,我们稍做调整,并增加函数的文档说明,如下:
>>> def trapezoidal_area(upperLength,bottom,height):
... \"\"\"函数说明:输入:长、宽、高
... 返回该梯形的面积\"\"\"
... return (upperLength+bottom)*height/2
...
>>> trapezoidal_area(3,4,5) # 按定义的顺序对应 upperLength=3,bottom=4,height=5
17.5
>>> trapezoidal_area(upperLength=3,bottom=4,height=5) #显式的指定参数的值,位置可以变化
17.5
>>> trapezoidal_area(bottom=4,height=5,upperLength=3) #显式的指定参数的值,位置可以变化
17.5
>>>
可以使用 help 函数查看该函数的文档说明:
>>> help(trapezoidal_area)
Help on function trapezoidal_area in module __main__:
trapezoidal_area(upperLength, bottom, height)
函数说明:输入:长、宽、高
返回该梯形的面积
参数带默认值的函数
在调用此函数传递参数的时候使用参数关键字,这样参数的位置可以任意放置而不影响运算结果,增加程序可读性。假如待计算的梯形默认高度都为 5,可以定义带默认值参数的函数。
>>> def trapezoidal_area(upperLength,bottom,height=5):#定义默认值参数
... return (upperLength+bottom)*height/2
...
>>> trapezoidal_area(upperLength=3,bottom=4)
17.5
>>> trapezoidal_area(3,4)
17.5
>>> trapezoidal_area(3,4,5)
17.5
>>> trapezoidal_area(3,4,10)
35.0
注意:带有默认值的参数必须位于不含默认值参数的后面 。
参数个数不固定的函数
你可能需要一个函数能处理比当初声明时更多的参数,此时你可以定义不定长参数,语法如下:
def 函数名([固定参数列表,] *不固定参数名 ):
\"函数_文档字符串\"
函数体
return [expression]
加了星号 * 的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。
举个例子:
#!/usr/bin/python3
# 可写函数说明
def printinfo( arg1, *vartuple ):
\"打印任何传入的参数\"
print (\"输出: \")
print (arg1)
for var in vartuple:
print (var)
# 调用printinfo 函数
printinfo( 10 ) #不向函数传递未命名的变量
printinfo( 70, 60, 50 ) #向函数传递未命名的变量
输出结果为:
输出:
10
输出:
70
60
50
还有一种就是参数带两个星号 **的参数会以字典的形式传入:
#!/usr/bin/python3
# 可写函数说明
def printinfo( arg1, **vardict ):
\"打印任何传入的参数\"
print (\"输出: \")
print (arg1)
print (vardict)
# 调用printinfo 函数
printinfo(1, a=2,b=3)
输出结果为:
输出:
1
{\'a\': 2, \'b\': 3}
声明函数时,参数中星号 * 可以单独出现,例如:
def f(a,b,*,c):
return a+b+c
如果单独出现星号 * 后的参数必须用关键字传入。
>>> def f(a,b,*,c):
... return a+b+c
...
>>> f(1,2,3) # 报错
Traceback (most recent call last):
File \"<stdin>\", line 1, in <module>
TypeError: f() takes 2 positional arguments but 3 were given
>>> f(1,2,c=3) # 正常
6
>>>
是值传递还是引用传递?
关于函数是否会改变传入变量的值分两种情况:
(1)对不可变数据类型的参数,函数无法改变其值,如字符串,数字,元组等。
(2)对可变数据类型的参数,函数可以改变其值,如列表,字典,集合等。
这里什么是可变数据类型,什么是不可变数据类型,请参考上一篇文章Python 的可变/不可变数据类型。
请尝试说出下面程序的输出结果:
# !/usr/local/bin/python3
# -*- coding: utf-8 -*-
# Time: 2018/10/6 7:36:38
# Description:
# File Name: lx_fun_params.py
def change_nothing(var):
var = \"new value\"
def try_change(var):
if type(var) is list:
var.append(\"new value\")
elif type(var) is str:
var = var + \" new value\"
else:
pass
def try_change1(var):
var = var+\"a\"
str1 = \"old value\"
list1 = [\"old value\"]
change_nothing(str1)
change_nothing(list1)
print(\"after call change_nothing:\")
print(str1)
print(list1)
#恢复原值
str1 = \"old value\"
list1 = [\"old value\"]
try_change(str1)
try_change(list1)
print(\"after call try_change:\")
print(str1)
print(list1)
按照 C/C++ 的思维会产生函数参数是值传递,还是引用传递。有些同学可会潜移默化的认为列表是属于引用传递, change_nothing 调用之后 str1 未被改变,list1 变成字符串 “new value\”, try_change 调用之后 str1 未被改变,list1 会新加入元素 “new value\”。
真正的结果是:
image.png
Python 函数参数的传递既不是所谓的传值也不是传引用。如果你理解发什么是可变数据类型 ,什么是不可变数据类型,这就很好理解。请牢记,在 Python 世界里,万物皆对象,变量是对象的引用,代表着对象在内存中的地址。Python 中函数参数传递的是变量的值,即就是变量所指向的对象的地址。
对上例中的字符串 str1 ,如下图所示:在调用 change_nothing 传入参数时前,str1 与 var 均指向 \”old value\” 的地址,调用 change_nothing 后,var 指向了新的对象 \”new value\”,因此 str1 未发生任何变化,对字符串 str1 调用 try_change 的本质与 change_nothing 是一样的,同样都是赋值操作,因此 str1 均不发生变化。
image.png
list1 也是同样的道理,因此在调用 change_nothing 之后,list1 的值仍然是 [\”old value\”]
但是在调用 try_change 函数时,发生了变化。如下图所示
image.png
开始传参时 list1 和 var 均指向 [\”old value\”],由于列表是可变数据类型,增加、删除、修改元素时不产生新的对象,对象在内存中的地址不发生变化,var 仍指向原来的 list1 的地址,因此在调用 try_change 函数后,list1 被改变。
涉及到的其他小知识:
(1)isinstance 和 type 的用法:
python 判断一个变量属于什么对象可以使用 isinstance 和 type,二者的区别在于判断有继承关系的类时
isinstance 认为子类是父类,type 则认为子类不是父类,如下所示:
class A:
pass
class B(A): # B 是 A 的子类
pass
isinstance(A(), A) # returns True
type(A()) == A # returns True
isinstance(B(), A) # returns True
type(B()) == A # returns False
(2)匿名函数:
python 使用 lambda 来创建匿名函数。
所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。
语法
lambda 函数的语法只包含一个语句,如下:
lambda [arg1 [,arg2,.....argn]]:expression
例子:
#!/usr/bin/python3
# 可写函数说明
sum = lambda arg1, arg2: arg1 + arg2
# 调用sum函数
print (\"相加后的值为 : \", sum( 10, 20 ))
print (\"相加后的值为 : \", sum( 20, 20 ))
以上实例输出结果:
相加后的值为 : 30
相加后的值为 : 40