第八章 函数

在Python中,函数是事先组织好的、可重复使用的功能代码段。合理使用函数,能显著提高代码的复用率和开发效率。在本章中,我们将主要讨论Python函数的定义、函数的参数(包括关键字参数、可变参数、默认参数等)“花式”传递、函数的递归调用及函数式编程方法(包括 lambda表达式、filter()函数、map()函数和 reduce()函数等)

有了前面章节的语法铺垫,现在我们已经可以动手编写简单的Pyho程序了。但随着代码越写 越多,我们会发现,很多代码的功能其实非常相似,但这些代码却被重复地输入和执行。于是,人们开始考虑,能否将这些功能特定、重用频率高的代码段封装起来呢?于是,函数的概念就出现在编程语言中了。

函数定义

在中文里,“函数”(Function)一词最早是由清末翻译家李善兰创造出来的。李善兰在翻译西 方数学著作时,根据对应变化关系发明了这个名词,他讲:“凡此变数中函(即蕴涵之意)彼变数者,则此为彼之函数。”

李善兰笔下的函数,侧重于数学意义上的解释。但计算机编程领域更侧重表达它的第二层含义 —— 完成某项具体功能的代码段。无功能,无以谓之函数。

在前面的范例中,我们已经多次使用了Python 的内置函数 (Build in Function,简称BIF) ,如 print()、 len()、range()等。但有时,标准化的内置函数并不能满足我们的 个性化功能需求 ,这时就需要我们自己创建函数,即用户自定义函数。 使用自定义的函数能显著提高代码的重复利用率和程序的模块化水平。那么,在 Python 中,该如何自定义一个函数呢?要做到这一点,需要遵循如下四个简单规则。

  1. 函数代码块以 def 关键字开头,后接函数标识符名称 和 圆括号 ()
  2. 传入参数须放入 圆括号内, 不同参数用逗号隔开。 即使一个参数也没有,这个圆括号也必须保留
  3. 函数体 必须 以冒号 : 起始, 函数的作用范围要按规定统一缩进
  4. 以 return[表达式] 结束函数,选择性地返回某个 特定值给调用方。 如果不写 return 表示式,系统会自动返回一个默认值 None

def 函数名( [参数列表] ):
    ''' 函数文档注释 ```
    函数体

函数名、参数类型及其出现顺序,构成了一个函数的“签名”。 在调用函数时,除了函数名必须正确,函数的实参和形参也需要按函数声明中定义的顺序一一匹配。也就是说,“函数签名”必须一致,验明正身,函数才能被正确调用。

# 计算面积函数
def area(width, height):
    return width * height

w = 4
h = 5
print("area",area(w,h))
>>> area 20

代码分析 在上述代码中,定义了名为area的函数,然后调用了这个函数。 - 在这个例子中,width 和 height 是函数的形式上的参数,简称形参。 - 参数w和h有实实在在的值(分别为4和5),称为实参。

实现功能就是,给定矩形的长和宽,求矩形的面积。 但有别于其他编程语言的地方在于,在Python中,函数定义的形参类型并不需要提前声明。当实参给形参赋值时,实参的类型就是形参的类型。如area(w,h),由于实参w和h是整型,于是形参width和height的类型就是整型。

如果实参的类型发生变化,形参的类型也会随之发生改变。 比如,如果实参 w 和 h 的值分别是 4.0 和 5.0,那么函数调用时,形参 width 和 height 的类型就是浮点型。 函数的母体 area 以“不变应万变”之势,等待不同类型实参的到来。这个态势,非常类似于C++等语言中的模版函数(Template Function)的功能。

其实,我们还可以定义一个什么都不做的空函数,其中用 pass 语句代替函数内部的代码块(类似于C、C++、Java中的“;”)

def do_nothing():
    pass # 空语句
>>>

实际上,pass 可以作为一个占位符 , 用来视为未成熟代码的“预留地”。 比如说,如果我们还没想好函数的内部实现,就可以先放一个 pass , 让代码先跑起来。 pass 语句还可以用在其他语句里。比如说,在 if 语句里,如果还没有想清楚在符合什么条件下干什么事,就可以用pass暂时“蒙混过关”。

if num >= 100:
    pass

在这个 if 条件内, 如果缺少了 pass 代码就会产生语法错误而无法运行

函数返回

前面我们提到,Python的函数可以利用 return 语句,选择性地返回一个特定值给调用方。如果没有使用 return 语句,系统会自动返回一个默认值 None.

现在我们的需求是: 让Python 函数返回多个值,那么这个需求可以实现吗? 从效果上看,是可以的;从本质上讲,是不行的。

我们先说第一个层面,从效果上看,通过Python语法糖的包装,的确可以达到让return语句返 回多个值的目的 ->

def return_mul_val():  
   my_str = "Hello Pyhton"  
   num = 20  
   return my_str, num # 返回 my_str 和 num 两个值  


str_, x = return_mul_val() # 用 str 和 两个变量接受函数返回的两个值  
print(str_, x)

>>> Hello Pyhton 20

代码分析 从形式上看,代码返回了多个值,而从输出效果来看,返回的值也的确被正确解析出来了。但这其实只是一种假象。 在本质上,Python函数返回的仍然是单一值。为什么这么说呢?

在前面的章节中,我们提到,对于元组而言,逗号甚至比那对圆括号更具有身份象征意义。 在Pythor语法上,为了书写方便,去掉包裹元素的圆括号而仅保留逗号也能定义一个元组。 根据这样的规定,返回的实际上是“一个”元组一 (mystr_,num) 这里描述的重点是量词 ''一个''!如果细究Python语法,可以发现,,等号(=)左边的 str, x 实际上也被Python定义为一个置名的元组了。 这样一来,第06行完成的实际上是两个元组之间的赋值。而元组之间的赋值,其实就是按照元素对应(element-wise)位置一 一赋值的

return my_str, num # 返回 my_str, num两个值

以上返回一个元组,实际上,返回的是元组的引用(即它在内存中的编号)。 为了方便理解,这里我们把内存编号比作宾馆房间的门牌号。 假设现在我们规定,服务员一次性只能处理一个房间号(类似于函数只能返回一个值),而一个房间号通常只对应一个人。于是,我们就可以得出一个“临时性”的结论:服务员一次只能接待一个人。 但如果服务员处理的是总统套房的房间号呢? 从表面上来看,服务员还是一次只能处理一个房间号,并没有违反规定,但接收方一旦收到这个总统套房的房间号,就可以按照套房中的内部结构,“按图索骥”,找到套房内各个小房间里的人,从而“间接”达到一次服务多个人的目的,类似于函数可以一次返回多个值。这种打包返回多个值的行为,可称为“集装箱”参数返回。

类似地,我们可以利用函数返回一个列表、一个字典、一个集合等。而列表、字典、集合等都 属于复合数据类型,它们内部都可以包含多个元素。如此一来,同样能达到Pyho函数返回多个值的目的。

函数文档的构建

前面的章节中,我们已经学习了Python中的注释方式,单行注释以 # 开头,进行多行注释时通常用三个单引号( ''' )将注释部分包裹起来。

在函数的定义中,常利用多行注释给函数写文档,称为函数文档。为什么要给函数写文档呢?

函数实现其对应的功能不就大功告成了吗?当然没有这么简单。 在复杂系统的开发流程中,大规模协助是常态。有时我们必须和他人进行团队合作才能完成大项目的开发,这时就有一个很迫切的需求一与你合作的他人必须能看懂你的代码。

在程序员的世界里,有这么一个笑话:“刚写的代码只有我和上帝能看懂,一个月之后,就只有上帝能看懂了。”

本质上,代码是程序员思维方式的一种物化形式。不太严谨地说,这世界上,没有比理解另一位程序员的思维更加困难的事情了。 因此,为Python代码写文档,增强程序的可读性和可用性,是 非常重要的,也是程序员的专业化素养。

给函数添加注释的目的在于,描述如何使用这个函数。下面我们先来感受一下Python官方给出的文档注释范本。 比如说,假设我们不知道字符串对象 str 的使用信息,于是我们需要寻求帮助。 在前面的章节中,我们学习到了一个通用的技巧,即对于不熟悉的函数,只要在命令行中使用帮助函数 help ()

help(int)
Help on class int in module builtins:

class int(object)
 |  int([x]) -> integer
 |  int(x, base=10) -> integer
 |  
 |  Convert a number or string to an integer, or return 0 if no arguments
 |  are given.  If x is a number, return x.__int__().  For floating point
 |  numbers, this truncates towards zero.
 |  
 |  If x is not a number or if base is given, then x must be a string,
 |  bytes, or bytearray instance representing an integer literal in the
 |  given base.  The literal can be preceded by '+' or '-' and be surrounded
 |  by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
 |  Base 0 means to interpret the base from the string as an integer literal.
 |  >>> int('0b100', base=0)
 |  4
 |  
 .....

从上面的输出结果可以看出,help() 函数功能非常强大。就如遇见生僻字而身边恰好有一本字典一样,甚是方便。

但追本溯源,为什么help()函数能输出str对象的帮助信息呢? 实际上,功劳还是归str自己,str事先准备了详细的文档,help()函数不过是“照本宣科”输出这部分文档罢了。那我们能不能也写出这样具有“专业范儿”的帮助文档呢?答案当然是可以的。

#计算面积函数  
def area(width, height):  
    '''  
    功能:计算矩形的面积  
    参数    :param width:数值型  
    矩形的宽    :param height:数值型  
    矩形的高度    :return:数值型  
    矩形的面积L width * height  
    '''  
    rec_area = width * height  
    return rec_area  

w = 4  
h = 5  
print("area:",area(w,h))

添加了函数文档(一种特殊的注释)。通过运行上述程序,函数已经运行在内存之中。此时,假设你是一个对该函数用法不甚了然的用户,若想知道该函数的功能及使用方法,仅在命令行输人help(area)就可以查询函数的使用信息了.

help(area)

area.__doc__

函数参数的传递

关键字参数

在Python中,函数的参数分为两种:

一种是前面提到的普通参数,也被称为位置参数(Positional Argument) 言外之意是,参数的位置非常重要,在调用函数时,实参的顺序和类型,要和形参的顺序和类型一一对应,否则就会报错。 但有时,粗心的程序员会把参数的位置搞错,从而导致调用失败。 于是,提供了一颗“语法糖”一关键字参数(Keyword Argument)。关键字参数亦称命名参数(Named Arguments) 相比于位置参数的位置是至关重要的,在关键字参数中,位置无关紧要,但参数(即形参)的名称非常重要。也就是说,有了关键字(参数名称)的标定,即使参数顺序变了,解释器依然能“按图索骥”找到实参和形参的对应关系,参见如下代码:

def saySomething(name, word): # 定义一个包括两个参数的函数  
    print(name + ':'+ word)  

saySomething("zs","lsb") # 正常顺序调用  

saySomething(word="zs",name="lsb") # 故意不小心的调用

>>> zs:lsb
>>> lsb:zs

可变参数

存在可变参数(Variable Parameter)的情况是指,在函数调用时,其参数的个数并不固定。 在C、C++或Java中,可变参数应用得非常普遍。比如说,在C、C++中,我们在使用 printf()函数时,无论传递多少参数,只要格式正确,程序都不会报错。

在Python中,其实也能很方便地使用可变参数。 Python中的可变参数的表现形式为,在形参前添加一个星号(*),意为函数传递过来的实参个数不定,可能为0个、1个,也可能为n个(n≥2)。

需要注意的是,不管可变参数有多少个,在函数内部,它们都被“收集”起来并统一存放在以形参名为某个特定标识符的元组之中。 因此,可变参数也被称为“收集参数”。参见如下代码:

def varParaFun(name, *args):  
    print("位置参数",name)  
    print("收集参数",args)  
    print("第一个收集参数是",args[0])  
varParaFun("张三",111,222,333,666)

>>> 位置参数 张三
>>> 收集参数 (111, 222, 333, 666)
>>> 第一个收集参数是 111
  • 形参 arg就是元组 , 前面的星号(*)并不是形参的一部分,而是用来标识args是一个可变参数的。
  • 需要说明的是,在函数参数列表中,常用 *args来表示可变参数,实际上args就是一个形参名,它可以是任何合法的Python名称。
  • args是arguments(参数)的英文简写,这里常用这个名称,就是因为它具有可读性。

下面,我们再来看一个更加实用的可变参数示例,以便更加感性认知可变参数带来的便利:

def mySum(*args):  
    sum = 0  
    for i in range(0,len(args)):  
        sum += args[i]  
    return sum  


# 可变参数函数调用  
print(mySum(1,2,3,4,5))  
print(mySum(20.1,30.2))

>>> 15
>>> 50.3

代码分析 如前所述,可变参数被打包成了一个元组。而读取元组元素的格式就 是 元组名[索引]。所以,在for循环中,我们先用全局函数 len()读取元组中的元素个数(即长度),然后用读取元组中元素的方法 ,逐一读取数据并求和。

除了用单个星号(*)表示可变参数,其实还有另一种标定可变参数的形式,即用两个星号(**)来标定。 通过前文的介绍,我们知道,一个星号(*)将多个参数打包为一个元组,而两个星号(**) 的作用是什么呢? -> 它的作用就是把可变参数打包成字典模样

Python中没有指针的概念, * 仅仅是身份标识——可变参数

这时调用函数则需要采用如 argl=valuel,,arg2=value2 这样的形式。 等号左边的参数好比字典中的键(key) , 等号右边的数值好比字典中的值(value),示例代码如下:

def varFun(**x):  
    if len(x) == 0:  
        print("None")  
    else:  
        print(x)  


varFun() # 0个参数  
varFun(a=1, b=2) #有2个参数, 以键/值 对将可变参数存放在字典中

>>> None
>>> {'a': 1, 'b': 2}

分析 - 从上述代码的输出结果可以看出,以两个星号 ** 标定可变参数时,表明可变参数是字典元素。 - 在调用时,参数必须成对出现,并用等号区分键和值,这时如果我们还使用传统的参数赋值方式,如 varFun(1,3) 编译器是不能通过的。

除了用等号给可变关键字参数赋值,事实上,我们还可以直接用字典给可变关键字参数赋值。

def some_kwargs(name, age, sex):  
    print("姓名",name)  
    print("年龄",age)  
    print("性别",sex)  

kwargs_dic = {'name':'Alice', 'age':11, 'sex':'女'}  


print(some_kwargs(**kwargs_dic))

>>> 姓名 Alice
>>> 年龄 11
>>> 性别 女

在形式上,可变数量的关键字参数调用,有点类似于带有默认参数值的参数调用。下面,我们就顺便讨论一下带有默认参效的函数使用方法。

默认参数

在函数定义时,函数中某些形参被事先赋予了默认值,这类带有默认值的形参,称为默认参数。

用户在调用函数时,如果给定的实参“不守规矩”,提供的实参不符合该怎么办呢?

此时,按照普通位置参数的调用规则,函数调用是不成功的。 为了避免出现这种情况,可在函数定义时提前给某些形参赋予一个默认值。这样的参数就相当于“替补队员”,能在实参缺位时及时补上。

def defautFun9(x,y = 3): # 给 y 设定一个默认值 3    print(x,y)  

defautFun9(1,5) 
# 正常调用, 给足两个参数,参数y被赋值为5,覆盖默认值3  
defautFun9(1) 
# 默认值调用,只给一个参数,第二个参数采用事先给定的默认值3

>>> 
>>> 1 5
>>> 1 3

查看默认值

我们可以使用“ 函数名. __defaults__ ”的形式来查看某个函数中参数的默认值,如下所示。

print(defautFun9.__defaults__)
>>> (3,)

函数的默认参数很好用,但如果使用不当,也会“莫名其妙”掉到坑里。

举个例子,假设我们想定义一个函数,传人一个列表(这个列表默认是空列表),添加一个字符串END再返回,则该函数可以如下定义。

def add_end(L=[]): # 默认参数L为空列表  
    L.append('END')  
    return L  

print(add_end(['Hello','Python']))
>>> ['Hello', 'Python', 'END']

当我们启用默认参数时,就会出现问题,如下:

def add_end(L = []): # 默认参数 L 为空列表
    L.append('END')
    return L

# 首次调用,没用提供实参,启用默认参数
print(add_end() )

# >>> ['END']

print(add_end() )
# ['END', 'END']

print(add_end() )
# ['END', 'END', 'END']

解读

  • 可能会有读者困惑,明明列表L的默认参数是 [] 但函数 add_end似乎每次都“记住了”上一次调用后添加 END 的列表。
     
  • 关于以上问题,是因为,Python函数在刚定义时,默认参数L的值就已经被计算出来了,即空列表([])。
     
  • 函数对象 add_end 和它的属性 —— 默认参数L同时存在于内存中。
     
  • 但由于列表L是一个可变量(mutable), 每次调用该函数时,如果改变了L的内容,则下次调用时,默认参数就会改变,不再是函数刚开始定义时的那个空列表( [] )了。
     

那么该如何避免这种情况呢

  • 我们需要记住的是,在定义默认参数时务必要让这个默认参数是 ==不可变对象==(immutable) . 比如说: 数值型、元组、字符串、不可变集合(frozenset)、None等。

对于上面的示例,我们可以用None这个不变对象来加以修正,代码如下。

def add_end(L = None): # 设定默认参数为不可变对象 None
    if L is None:
        L = []
    L.append('END')
    return L

print(add_end() )

# ['END']

print(add_end() )
# ['END']

print(add_end() )
# ['END']

解读

使用修正后的默认参数函数 add_end 后,无论调用多少次 add_end ,都会有正确的输出。使用诸如 str、元组等不可变对象(类似于C++、Java中的常量类型)

  • 好处
  • 不可变对象一旦创建后,对象内部的数据就不能修改,这样就减少了由于修改数据而造成的错误。
  • 此外,由于对象是不可变的,在多任务环境下同时读取对象不需要加锁来避免多用户写数据带来的延迟,同时对读数据也没有影响。
  • 因此在编写程序时,只要条件允许,要尽可能将操作对象设计成不可变对象。

参数序列的打包与解包

在前面的章节中,我们介绍了两种可变参数的标记方式:

  1. 利用一个星号(*)构建一个参数元组;
     
  2. 利用两个星号(**)构建参数字典。
     

事实上,在函数参数传递过程中,还有一种看似类似实则不然的参数传递方式。说它“类似”,是因为在外观上它也在参数前打上一个星号(*)。说它“不然”,是因为这种操作的内涵不同:星号(*)是作用在实参上的:实参是有讲究的,这些实参主要包括列表、元组、集合、字典及其他可 迭代对象。

如果在这类实参前面加上一个星号(*),那么Python解释器就会对这些可迭代对象进行解包 (unpacking,亦有文献译作“解压”),然后将解包后的元素一 一分配给多个形参。 说到解包,我们先介绍一下它的反操作一打包(packing)

  • 参见如下代码

val = 1,2,3,4
print(type(val))
# <class 'tuple'>

print(val)
# (1, 2, 3, 4)

解读

在输入处,表达式等号的右边分别是四个零散的整型数 1,2,3,4,然后赋值给了val对象。

  • 通过元组,Python将等号右边的四个整型数“打包”成了一个匿名的元组,然后赋值给val。
     
  • 另一方面,Python变量的类型并不需要事先声明,而是通过赋值得到的。
     
  • 通过赋值操作,将等号右边的变量类型赋给等号左边的对象即可。如此,val的类型就被定义为一个元组了。
     
  • 上述判断 从输出结果中得到印证。
     

在输出Out[2]中,元组的另外一个标志一那对圆括号()也被Python解释器自动加上了。

Question

问题:如果我们把元组作为一个整体给分散对象赋值,那么这个打包元组中的元素会被 一 一 解析出来吗?延续前面变量val的赋值,请参考如下代码:

val = 1,2,3,4
print(type(val))
# <class 'tuple'>

print(val)
# (1, 2, 3, 4)

a, b, c, d = val

print(a, b, c, d)
# 1 2 3 4
print(type(a)) # <class 'int'>
print(type(val))# <class 'tuple'>
val = 1,2,3,4
print(type(val))
# <class 'tuple'>

print(val)
# (1, 2, 3, 4)

a, b, c, d = val # mark

print(a, b, c, d)
# 1 2 3 4
print(type(a)) # <class 'int'>
print(type(val))# <class 'tuple'>

①mark:等号的左边也被封装为一个元组。因此,在本质上,代码完成的是两个元组之间的赋值。不过等号左边的元组没有它的“黄马甲”一一对圆括号护身,看起来等号右侧的元组元素被解包了。

解读

观察输入处,通过等号可将右侧的元组 val 一 一 对应赋值给等号左侧的四个变量。

在其他编程语言中,一对四的赋值方式通常是不被允许的。但在Python语法糖的包装下,上述方式是合法的。

通过 print() 输出验证,变量a、b、c、d的值均可正常输出。 用全局函数 type() 测试a的类型,可以看出,a的类型也是正确的(int),并非val的元组类型。

解包

  • 我们把这种将可迭代对象的元素分别赋值为分散对象的过程,称为解包。
  • 关于解包,需要注意的有两点:
     
  • 被解包的序列中的元素数量必须与赋值符号(=)左边元素的数量完全一样,否则就会报错。参见如下代码。
     

val = 1,2,3
print(type(val))
# <class 'tuple'>

print(val)
# (1, 2, 3)

a, b, c, d = val

print(a, b, c, d)

# ValueError: not enough values to unpack (expected 4, got 3)
  • 在赋值处,元组val内包含三个元素,分别是1、2、3。但,等号右侧有四个变量(分别是a、b、c、d)等着被赋值,解包元素的数量不够!因此Python解释器会“毫不客气”地指出问题所在:没有足够的值来解包。
     
  • 支持解包操作的不仅限于元组,也包括所有可迭代的对象,比如列表、字典等。
     

于是,我们想知道,这种自动解包的行为能否也在函数参数传递时发生?比如说,如果实参为一个列表或元组,它会自动解包,将其内的元素一一分配给不同的形参吗?

```py def run(a,b,c,d): #定义有四个参数 print(a,b,c,d)

my_list = [1,3,4,5] # 定义一个包括四个元素的列表

run(my_list)

# TypeError: run() missing 3 required positional arguments: 'b', 'c', and 'd' ```

解读

  • 上述代码定义了一个函数 run(),它有四个形参a、b、c、d。然后又定义了一个包含四个元素的列表 my_list
  • 把 my_list 作为实参,通过解包操作给四个形参赋值,即分别让a=1、b=2、c=3、d=4。
  • 但输出结果让我们失望了,Python系统好像并不认可这种“简单粗暴”的参数解包行为”。
  • 那有没有办法让这种参数解包行为成功呢?

其实,我们距成功仅一步之遥。类似于可变参数,只需要在可迭代对象前打上一个星号(*),一切就都可以完美解决了,参见如下代码。

def run(a,b,c,d): #定义有四个参数
    print(a,b,c,d)


e = {'a':1, 'b':2, 'c':3, 'd':5}
run(*e)
run(**e)
#a b c d
#1 2 3 5
  • 解读

通过上面的输出结果可以看出,程序运行正常,并无语法错误,但输出结果不是我们想要的,以上代码仅仅把形参名输出了。 那该如何修正呢?如同前文讲解的那样,对于由字典构成的可变参数,我们用两个星号(*)表示,这里对字典的解包、也需要在字典名称前加上两个星号(**),示例代码如下。

  1. run(**e) #在字典对象前面加两个星号(**)
  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值