在中学数学中我们知道 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( 706050 ) #向函数传递未命名的变量

输出结果为:

输出: 
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 1in <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 均不发生变化。

Python 基础系列--函数_javaimage.png
list1 也是同样的道理,因此在调用 change_nothing 之后,list1 的值仍然是 ["old value"]


但是在调用 try_change 函数时,发生了变化。如下图所示

Python 基础系列--函数_java_02image.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( 1020 ))
print ("相加后的值为 : ", sum( 2020 ))

以上实例输出结果:

相加后的值为 :  30
相加后的值为 :  40