我们已经见过不少函数,也自己写过一些函数。我们已经理解函数的概念来自代数:从输入参数出发,计算出函数的返回值;我们也知道可以用 def foo():
来定义函数。其实函数的定义非常复杂,我们不太能够在第一次介绍时就讲清楚,所以之前我们就采取“先引入用起来”的方法,这也是一种知识上的“提前引用”。
这一章我们就围绕函数定义深入看看。
哪怕一个函数内部什么都不干,它也得有个名字,然后名字后面要加上圆括号 ()
,以明示它是个函数,而不是某个变量。
def do_nothing():
pass
do_nothing()
这就是个“什么也不干”的函数,关键字 pass
就是什么也不干的意思。
给函数命名(给变量命名也一样)需要遵循的一些规则如下:
_
;do_nothing
),也可以用所谓 Camel Case 风格(doNothing),习惯上更推荐使用下划线;最后这一条,关键字也叫保留字(reserved),是编程语言保护起来内部使用的,如果程序用这些词儿做变量或者函数或者类型名字,编译器或者解释器就无法正确工作了。Python 提供了一个模块叫 keyword
来帮助我们了解语言有哪些关键字:
import keyword
keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
keyword.kwlist
就是当前你使用的 Python 解释器中不可使用的关键字列表,如果我们不记得这个列表,可以随时用 keyword.iskeyword('xxx')
来查询某个词是不是关键字。
在程序里给变量、函数命名是个挺重要的事情,影响到程序的可读性,就像小说的语言,最好能有一种流畅清晰、又始终一致的风格(style)。为了让全世界的 Python 程序员都有相对一致的风格,Python 社区有专门的一套建议规范,放在专门维护 Python 语言特性的社区 PEP 上:
PEP,是 Python enhancement proposal 的缩写,每当有重要的语言特性新需求新想法,就放在这里,经过广大 Python 用户和开发者的讨论完善,在某个版本放进 Python 中。很多 PEP 早已从 proposal 毕业变成官方特性,但也还在这里保留着。PEP 8 就是一个古老的 proposal,现在已为大多数 Python 用户采纳。
函数可以没有参数,也可以有一个或者多个参数。
没有参数就意味着,这个函数执行不依赖于输入,比如我们定义一个函数来在程序结束时打印一句退出提示:
def exit_info():
print('Program exits. Bye.')
exit_info()
Program exits. Bye.
注意即使没有参数,无论定义还是调用时,函数名后面的括号都是不可省略的,这是函数身份的标志。
函数也可以有多个参数,调用时输入参数的值是严格按照参数的顺序去匹配的。比如我们写一个函数输出某年到某年之间的所有闰年:
def leap_years(begin, end):
year = begin
while year < end:
if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
print(year)
year += 1
leap_years(2000, 2020)
2000 2004 2008 2012 2016
当我们调用 leap_years(2000, 2020)
时,输入两个参数值 2000 和 2020,按照顺序匹配函数定义 leap_years(begin, end)
,于是 begin = 2000
end = 2020
。所以参数的顺序是不能搞错的,有些函数参数很多,要是开发过程中还调整过顺序的话,那简直就是灾难,所以一般情况下还是保持函数参数不要乱动为好。
顺便说一句,判断闰年的算法虽然不难,但要写的简洁也不容易。建议你可以先自己思考和实现一遍,然后尝试搞清楚为啥上面代码里的那行 if
是对的。实际上闰年的判断有很多正确的写法,你应该尝试写出自己的版本并确认它的正确性。
和参数一样,Python 的函数可以没有返回值,也可以有一个或者多个返回值。
上面的 exit_info
和 leap_year
也是没有返回值的例子,它们的效果都通过 print
函数来体现。实际上没有返回语句的函数,等价于在其最后有一句 return None
,表示函数返回了一个空值 None
,None
在 Python 中是一个合法的值,表示什么都没有,它在逻辑上等价于 False
:
bool(None)
False
所以即使没有返回值的函数,也可以用在 if
后面做逻辑表达式,不过我们并不推荐这么做,因为可读性很差。
大部分情况下函数是有返回值的,因为绝大部分情况下函数的作用都是做“数据处理”,从输入出发得到输出。
一般情况下函数都只有一个返回值,我们已经见过不少例子;但 Python 也允许多返回值,比如我们想用一个函数来计算两个整数相除的商和余数,可以这么写:
def idiv(a, b):
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = idiv(50, 6)
print(q, r)
8 2
和多参数的情况类似,多返回值的情况下,赋值也是按照顺序匹配的,上面的代码中赋值语句左边的 q
匹配到第一个返回值,r
匹配第二个。
下面的代码经常会把人搞晕:
def increase_one(n):
n += 1
return n
n = 1
print(increase_one(n))
print(n)
2 1
请你思考一下,为什么这段代码里的两个 print
函数输出分别是 2 和 1。
这个问题就涉及到变量的作用域(scope)问题,也就是说在不同地方出现的同名变量和函数,可能是完全不同的两个东西:
这样我们就能理解上面代码输出的 2 和 1 了:
print()
打印的是函数调用 increase_one(n)
的返回值,这个语句不在任何函数定义体中,所以它里面用到的变量都是全局变量:increase_one()
时参数 n
,按照作用域原理,是全局变量 n
当时的值,也就是 1;increase_one()
函数定义内,参数 n
是输入参数即局部变量,带着传进来的值 1,经过加一之后返回,返回值是 2;print
打印这个返回值,输出 2;n
的值;print()
打印的是全局变量 n
的值,输出出 1。以上的文字,可能需要反复阅读若干遍;几遍下来,消除了疑惑,以后就彻底没问题了;若是这个疑惑并未消除,或者关键点并未消化,以后则会反复被这个疑惑所坑害,浪费无数时间。
顺便说一句,上面这个例子用来说明作用域的概念很有用,但是平时写程序最好别这么写,减少重名的变量可以提升代码的清晰度和可读性。
与此相关的,我们在介绍列表等数据容器时,会为上面的规则作出重要的补充,这里先留一个伏笔。
我们其实已经见过带缺省值的参数(argument with default value),这里我们更细致的看看这个特性。
在函数定义中可以在某个参数后面用等号 =
给它一个缺省值,调用时可以省略传入这个参数的值,直接采用缺省值;当然也可以在调用时传入这个参数的值来覆盖掉缺省值。这种特性相当于给了这个函数两个版本,一个带某个参数,一个不带,不带的版本就当该参数是某个缺省值。看看下面的例子:
def greeting(name, msg='Hi'):
print(f'{msg}, {name}!')
greeting('Neo')
Hi, Neo!
greeting('Neo', 'Good morning')
Good morning, Neo!
一个函数可以有多个带缺省值的参数,但有一个限制:所有这些带缺省值的参数只能堆在参数表的最后,也就是说你定义的参数表里,出现一个带缺省值的参数,则它后面的都必须带缺省值。如果把上面的 greeting()
函数的两个参数调换一下,会扔出一个 SyntaxError: non-default argument follows default argument
的异常。
我们前面说过,调用函数时传入的参数值会严格按照顺序去匹配参数变量,第一个输入值赋给第一个参数变量,第二个值赋给第二个参数变量,依此类推。因为有了上面说的带缺省值参数,这个规则出现了变通的可能。
如果一个函数有多个带缺省值的参数,我们想忽略掉某几个参数(就用其缺省值),但指定后面某一个参数的值(覆盖缺省值),例如下面这个函数:
def greeting(name, msg='Hi', punc='!'):
print(f'{msg}, {name}{punc}')
在这个版本的 greeting()
函数中,包含一个普通参数 name
和两个带缺省值的参数 msg
punc
,如果我们想跳过 msg
只传入 name
(这个是必须的,因为没有缺省值)和 punc
的值,那么就可用下面的语法:
greeting('Neo', punc='.')
Hi, Neo.
这里第一个值按照顺序位置匹配到参数变量 name
,这叫 positional argument(即“按照位置顺序匹配的参数”),而按照位置下一个是 msg
,是我们想跳过的,所以要注明参数变量名,说明下一个传入的值 '.'
是给 punc
参数变量的,这叫 keyword argument(即“按照参数名匹配的参数”)。
由于所有带缺省值的参数都在普通参数的后面,所以我们只要记住:
变量名=值
这样的格式在后面列出(keyword),未列出的就还用缺省值了。在后半部分,顺序就无所谓了,可以和定义时不一样,反正是用名字指定的(keyword),比如我们完全可以这么干:
greeting('Neo', punc='.', msg='Good nite')
Good nite, Neo.
到目前为止,Python 的函数定义还是很简单清晰的,无论参数还是返回值,都没什么难懂的。下面开始就要进入比较混沌的领域了。
所谓变长参数就是函数定义时名字前面带个星号 *
的参数变量,这表示这个变量其实是一组值,多少个都可以。我们先来看个简单的例子:
def say_hi(*names):
for name in names:
print('Hi,', name)
say_hi('Neo')
Hi, Neo
say_hi('Neo', 'Trinity')
Hi, Neo Hi, Trinity
say_hi('Neo', 'Trinity', 'Morpheus')
Hi, Neo Hi, Trinity Hi, Morpheus
在这个例子里,*names
是一个变长参数(arbitrary argument),调用时可以传入一个或者多个值,函数会把这些值看做一个列表,赋给局部变量 names
——后面我们会知道,其实不是列表(list),而是一个元组(tuple)——然后我们在函数体中可以用 for...in
来对这个 names
做循环。
有些中文书籍把 arbitrary arguments 翻译成“可变参数”或者“任意参数”。事实上,在这样的地方,无论怎样的中文翻译都是很难准确表达原意的。这还算好的,甚至还见过翻译成“武断的参数”的——这样的翻译肯定会使读者产生说不明道不白的疑惑。
所以,入门之后就尽量只用英文是个好策略。虽然刚开始有点吃力,但后面会很省心,很长寿——是呀,少浪费时间、少浪费生命,其实就相当于更长寿了呀!
在使用 arbitrary argument 的场合,有几点需要注意:
for...in
循环处理,用复数名词在写类似 for name in names
的循环语句时会很舒服、很地道(idiomatic),是的,写程序和学外语一样,不写则已,写就要尽量写得“地道”;上面的第二点,有一个不太常见的例外,那就是一个函数既有 arbitrary arguments 又有 arguments with default values 的情况,那么可以有两个 arbitrary arguments,其中第二个必须带缺省值,然后参数表排列成这样:
def monstrosity(*normal arguments*, *normal arbitrary argument*, *arguments with defaults*, *arbitrary argument with default*)
这样是完全符合语法要求的,调用时传入参数值还是按照前面讲的规则,先按照位置顺序匹配前两部分,多出来的都归 normal arbitrary argument;然后按照参数变量名指定对应值,没指定的都用缺省值。不过这实在是太麻烦了,不知道什么情况下才必须用这么可怕的函数,还是祈祷我们不会碰到这样的场景吧!
当然,只有上面列出的前三个部分的情况还是有的,比如下面的例子:
def say_hi(*names, msg='Hi', punc='!'):
for name in names:
print(f'{msg}, {name}{punc}')
say_hi('Neo', 'Trinity', punc='.')
Hi, Neo. Hi, Trinity.