在C#中没有独立的函数存在,只有类的(动态或静态)方法这一概念,它指的是类中用于执行计算或其它行为的成员。在Python中,你可以使用类似C#的方式定义类的动态或静态成员方法,因为它与C#一样支持完全的面向对象编程。你也可以用过程式编程的方式来编写Python程序,这时Python中的函数与类可以没有任何关系,类似C语言定义和使用函数的方式。此外,Python还支持函数式编程,虽然它对函数式编程的支持不如LISP等语言那样完备,但适当使用还是可以提高我们工作的效率。
本章主要介绍在过程编程模式下Python中函数的定义和使用方法,关于在面向对象编程中如何使用函数,我们将在下一章再讨论。此外,我还会简要介绍Python中的函数编程功能。
3.1 函数的定义
函数定义是最基本的行为抽象代码,也是软件复用最初级的方式。Python中函数的定义语句由def关键字、函数名、括号、参数(可选)及冒号:组成。下面是几个简单的函数定义语句:
1 # -*- coding: utf-8 -*- 2 #定义没有参数、也没有返回值的函数 3 def F1(): 4 print ‘hello kitty!‘ 5 #定义有参数和一个返回值的函数 6 def F2(x,y): 7 a = x + y 8 return a 9 #定义有多个返回值的函数,用逗号分割不同的返回值,返回结果是一个元组10 def F3(x,y):11 a = x/y12 b = x%y13 return a,b
可能你已经注意到了,Python定义函数的时候并没有约束参数的类型,它以最简单的形式支持了泛型编程。你可以输入任意类型的数据作为参数,只要这些类型支持函数内部的操作(当然必要时需要在函数内部做一些类型判断、异常处理之类的工作)。
3.2 函数的参数
3.2.1 C#与Python在函数参数定义方面的差别
C#中方法的参数有四种类型:
(1) 值参数不含任何修饰符
(2) 引用型参数以ref 修饰符声明(Python中没有对应的定义方式)
(3) 输出参数以out 修饰符声明(Python中不需要,因为函数可以有多个返回值)
(4) 数组型参数以params 修饰符声明
Python中函数参数的形式也有四种类型:
(1) f(arg1,arg2,...) 这是最常用的函数定义方式
(2) f(arg1=value1,arg2=value2,...,argN=valueN) 这种方式为参数提供了默认值,同时在调用函数时参数顺序可以变化,也称为关键字参数。
(3) f(*arg) arg代表了一个tuple,类似C#中的params修饰符作用,可以接受多个参数
(4) f(**arg) 传入的参数在函数内部是保存在名称为arg的dict中,调用的时候需要使用如f(a1=v1,a2=v2)的形式。
可以看出,Python函数参数定义与C#相比,最大的两个区别是支持关键字参数和字典参数。
3.29更新:根据JeffreyZhao提示,C# 4.0 已经支持命名参数(即关键字参数)和可选参数(即参数默认值),详情可见生鱼片blog上的:《C#4.0新特性:可选参数,命名参数,Dynamic》。在此向两位致谢。
3.2.2 关键字参数
关键字参数可以使我们调用含有很多参数、同时一些参数具有默认值的Python函数变得更简单,也是Python实现函数重载的一种重要手段(到类与对象部分再详细说这个问题)。下面的例子说明了如何定义和调用含关键字参数的函数(引自Guido van Rossum《Python入门》,包括后面的几个例子):
1 def parrot(voltage, state=‘a stiff‘, action=‘voom‘, type=‘Norwegian Blue‘):2 print "-- This parrot wouldn‘t", action,3 print "if you put", voltage, "Volts through it."4 print "-- Lovely plumage, the", type5 print "-- It‘s", state, "!"
可以用如下几种方式调用:
1 parrot(1000) # 缺省值2 parrot(action = ‘VOOOOOM‘, voltage = 1000000) # 关键字,缺省值,次序可变3 parrot(‘a thousand‘, state = ‘pushing up the daisies‘)# 位置参数,缺省值,关键字4 parrot(‘a million‘, ‘bereft of life‘, ‘jump‘) # 位置参数,缺省值
但以下几种调用方式是错误的:
1 parrot() # 非缺省的参数没有提供2 parrot(voltage=5.0, ‘dead‘) # 关键字参数后面又出现了非关键字参数3 parrot(110, voltage=220) # 参数值重复提供4 parrot(actor=‘John Cleese‘) # 未知关键字
3.2.3 字典参数
如果形参表中有一个形为**name的形参,在调用时这个形参可以接收一个字典,字典中包含所有不与任何形参匹配的关键字参数。例如下面的函数:
1 def cheeseshop(**keywords):2 for kw in keywords.keys(): print kw, ‘:‘, keywords[kw]
就可以象下面这样调用:
1 cheeseshop(client=‘John Cleese‘,2 shopkeeper=‘Michael Palin‘,3 sketch=‘Cheese Shop Sketch‘)
结果显示:
client : John Cleese
shopkeeper : Michael Palin
sketch : Cheese Shop Sketch
3.2.4 函数参数调用的顺序
调用Python函数时,参数书写的顺序分别为:非关键字参数,关键字参数,元组参数,字典参数。请记住以下几点规则:
* 通过位置分配非关键字参数
* 通过匹配变量名分配关键字参数
* 其他额外的非关键字参数分配到*name元组中
* 其他额外的关键字参数分配到**name的字典中
* 用默认值分配给在调用时未得到分配的参数
一般说来,实参表中非关键字参数在前,关键字参数在后,关键字名字必须是形参名字。形参有没有缺省值都可以用关键字参数的形式调用。每一形参至多只能对应一个实参,因此,已经由位置参数传入值的形参就不能在同一调用中再作为关键字参数。
总之,由于Python的函数参数定义和调用方式太灵活了,所以一开始容易把人搞晕。不过可以慢慢来,你会越来越发现Python的简便所在。
3.3 函数文档
在C#,可以使用文档XML标记对函数进行注释,这样在VS等IDE中,输入函数名后就会提示函数的功能、参数及返回值等的说明,方便函数的调用者。在Python中,也有类似的功能,即文档字符串(Docstrings)。但Python的文档字符串不像C#中的文档XML标记那么丰富,基本等同于C#中的<summary>标记,下面让我们一起来通过一个例子了解一下(引自《简明Python教程》):
1 def printMax(x, y): 2 ‘‘‘Prints the maximum of two numbers. 3 4 The two values must be integers.‘‘‘ 5 x = int(x) # convert to integers, if possible 6 y = int(y) 7 8 if x > y: 9 print x, ‘is maximum‘10 else:11 print y, ‘is maximum‘12 13 printMax(3, 5)14 print printMax.__doc__
输出
$ python func_doc.py
5 is maximum
Prints the maximum of two numbers.
The two values must be integers.
在上述函数中,第一个逻辑行的字符串是这个函数的文档字符串。文档字符串的惯例是一个多行字符串,它的首行以大写字母开始,句号结尾。第二行是空行,从第三行开始是详细的描述,描述对象的调用方法、参数说明、返回值等具体信息。
可以使用__doc__(注意双下划线)调用printMax函数的文档字符串属性(属于函数的名称,请记住Python把每一样东西都作为对象,包括这个函数)。它等同于用Python的内建函数help()读取函数的说明。很多Python的IDE也依赖于函数的文档字符串进行代码的智能提示,因此我们在编写函数时应养成编写文档字符串的习惯。
3.4 函数编程
Python中加入了一些在函数编程语言和Lisp中常见的功能,如匿名函数、高阶函数及列表内涵等,关于最后一个问题我在《2 运算符、表达式和流程控制》已经介绍过了,本章只介绍匿名函数和高阶函数。
3.4.1 匿名函数
lambda函数是匿名函数,用来定义没有名字的函数对象。在Python 中,lambda只能包含表达式:lambda arg1, arg2 ... : expression。lambda 关键字后就是逗号分隔的形参列表,冒号后面是一个表达式,表达式求值的结果为lambda的返回值。
虽然lambda的滥用会严重影响代码可读性,不过在适当的时候使用一下lambda 来减少键盘的敲击还是有其实际意义的,比如做排序的时候,使用data.sort(key=lambda o:o.year)显然比
1 def get_year(o):2 return o.year3 func=get_year(o)4 data.sort(key=func)
要方便许多。(引自《可爱的Python》)
3.29更新:根据JeffreyZhao提示,重新整理C#中匿名函数的内容(本部分主要参考MSDN,见http://msdn.microsoft.com/zh-cn/library/bb882516.aspx):
在C#中也有匿名函数功能。在 C# 1.0 中,通过使用在代码中其他位置定义的方法显式初始化委托来创建委托的实例。C# 2.0引入了匿名方法的概念,作为一种编写可在委托调用中执行的未命名内联语句块的方式。C# 3.0 引入了 Lambda表达式,这种表达式与匿名方法的概念类似,但更具表现力并且更简练。这两个功能统称为“匿名函数”。通常,针对 .NET 3.5 及更高版本的应用程序应使用 Lambda 表达式。
Lambda 表达式可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。所有 Lambda 表达式都使用 Lambda 运算符 =>, 运算符的左边是输入参数(如果有),右边包含表达式或语句块。可以将此表达式分配给委托类型,如下所示:
1 delegate int del(int i);2 del myDelegate = x => x * x;3 int j = myDelegate(5); //j = 25
3.4.2 高阶函数
高阶函数(High Order)是函数式编程的基础,常用的高阶函数有:
(1) 映射,也就是将算法施于容器中的每个元素,将返回值合并为一个新的容器。
(2) 过滤,将算法施于容器中的每个元素,将返回值为真的元素合并为一个新的容器。
(3) 合并,将算法(可能携带一个初值)依次施于容器中的每个元素,将返回值作为下一步计算的参数之一,与下一个元素再计算,直至最终获得一个总的结果。
Python通过map、filter、reduce三个内置函数来实现上述三类高阶函数功能。本文不对这三个函数的用法进行详细介绍,仅给出它们使用的示例代码(引自《Python的map、filter、reduce函数》),请细细体会:
1 2 #coding:utf-8 3 4 def map_func(lis): 5 return lis + 1 6 7 def filter_func(li): 8 if li % 2 == 0: 9 return True10 else:11 return False12 13 def reduce_func(li, lis):14 return li + lis15 16 li = [1,2,3,4,5]17 18 map_l = map(map_func, li) #将li中所有的数都+119 filter_l = filter(filter_func, li) #得到li中能被2整除的20 reduce_l = reduce(reduce_func, li) #1+2+3+4+521 22 print map_l23 print filter_l24 print reduce_l
运行结果如下:
C:\>python test1.py
[2, 3, 4, 5, 6]
[2, 4]
15
3.29更新:根据JeffreyZhao提示,C#可以用委托实现函数编程的大部分功能。不过说实话我没这么用过,因为是接触了Python后才有的函数编程概念,以前用C#写程序就压根没有函数编程思想,虽然知道"委托允许将方法作为参数进行传递"。但具体细节估计各位C#高手比我要清楚得多,我就不乱写了。
3.5 小结
本章讨论了Python中函数的定义和使用方法,要点如下:
(1) Python用def关键字、函数名、括号、参数(可选)及冒号定义函数(要记得函数体缩进呀),函数可以有0、1或多个返回值;
(2) Python定义函数不需要(也不能)指定参数类型,它天生就是泛型的;
(3) Python支持(单独或同时使用)普通参数、关键字参数、元组参数和字典参数四种类型的参数,请学会灵活使用它们;
(4) 文档字符串(Docstrings)用于对Python的函数提供注释,供自省函数及IDE调用;
(5) lambda关键字用来定义匿名函数,它仅支持表达式,某些情况下可以简化代码编写量,但不要滥用;
(6) Python通过map、filter、reduce三个内置函数实现函数编程中映射、过滤及合并三类高阶函数功能,但注意map和filter可以用列表内涵替代(参见第2章)。
进一步阅读的参考:
[1] 本章内容较多的参考了Python之父Guido van Rossum的《Python入门》以及国内的一本经典教程《可爱的Python》,再次向大家推荐这两本书,在此对作者及译者致谢。
[2] 限于篇幅和深度,关于Python函数中的一些重要主题没有讨论,例如作用域、函数嵌套、递归等,我的观点是先不要把事情弄得太复杂,慢慢来总是不错的。如果你已学会了本章的内容,推荐你进一步阅读《深入Python》,这本书中对很多主题的讨论都是(我看多的书中)最深入的。