本文综述 Python 函数的参数类型和捕获过程。parameter 的作用是捕获 argument,又叫做 参数处理机制(parameter handling mechanism),本文的主题围绕这一个捕获过程。 parameter 和 argument 的区别 在 这篇文章 ,解释了 parameter 和 argument 的概念上的区别。 parameter 出现在函数定义中;argument 出现在函数调用中。 可以用下面的例子解释: # 定义 def f(a): pass # 调用 f(1) 这个例子中,定义函数时的 a 是 parameter, 而调用函数时的 1 是 argument. 参数的捕获 当函数被调用的时候,如何让用户提供的 argument 被正确的赋值给 parameter, 是从 argument 到 parameter 的映射。 因为这个映射是个从左往右的有序的动作,所以更形象的说,是一个: parameters 捕获 arguments 的过程 argument values 填入 parameter slots 的过程 parameter 列表形成一个个的 slot,而 argument 包含若干个 value,通过一系列规定好的逻辑,将 value 捕获到 slot 中去,这就是参数处理机制(parameter handling mechanism),本文的主题围绕这一个捕获过程,系统的梳理一下参数处理机制。 通过位置捕获 最简单的捕获方式就是通过位置捕获,这和 shell 函数相同: # 定义 def f(a, b): pass # 调用 f(1, 2) 这里的 a、b 靠自身所处的位置捕获参数值,称为 Positional arguments. 这种通过位置对应获取参数的方式可以称为 隐式(implicit)的获取 匿名参数 参数值会按照位置一一对应到参数列表中的变量中去。如果提供的参数列表个数和定义的不符合,无论多还是少,都会抛出 TypeError: >>> def f(a,b): ... pass ... >>> f(1) Traceback (most recent call last): File "", line 1, in TypeError: f() missing 1 required positional argument: 'b' >>> f(1,2,3) Traceback (most recent call last): File "", line 1, in TypeError: f() takes 2 positional arguments but 3 were given >>> 位置参数的困难 当参数列表很长时,会遇到一个困难,就是需要逐个参数对应。比如 >>> def f(a,b,c,d,e,f,g): ... pass ... >>> f(1,2,3,4,5,6,7) >>> 如果想修改中间某个参数,需要先到参数列表中从左向右数出来参数的位置: ------> f(a,b,c,d,e,f,g) 然后再到 arguments 列表里从左到右数出来参数的位置。 ------> f(1,2,3,4,5,6,7) 这种定位参数的方式会让人很累,造成可读性差、易出错。在隐式获取基础上,需要一种显示的获取(explicit capture),也就是赋值时指定变量名称。 通过名称捕获 名称捕获又叫关键字参数,Keyword arguments. 通过变量名直接指定参数值。 举个例子: >>> def f(a,b,c,d,e,f,g): ... pass ... >>> f(1,2,3,4,5,6,7) >>> f(g=1,f=2,e=3,d=4,c=5,b=6,a=7) >>> 如果要修改某个参数值,可以方便的找到参数名称。 关键字参数的优点是能通过变量名称了解参数值的含义,提高了可读性。 位置参数和关键字参数的混合 当有些参数以 位置参数形式提供,有些参数以 关键字参数 形式提供时,会遇到新问题: 如果 keyword argument 放置于 positional argument 前面,会抛出 SyntaxError ,因为如果列表前面提供了参数名称后,后面的参数就不能根据位置捕获。 举个例子: >>> def f(a,b,c,d,e,f,g): ... pass ... >>> f(g=1,f=2,3,4,5,b=6,a=7) File "", line 1 f(g=1,f=2,3,4,5,b=6,a=7) ^ SyntaxError: positional argument follows keyword argument >>> 比如上面的例子里,参数值3就很难说希望被哪个参数捕获。所以得到一条原则: 原则1: 调用函数时,位置参数必须先于关键字参数提供。 基于这个原则,在设计函数时,需要把希望关键字参数放在参数列表后面。 位置参数和 关键字参数 两种参数属于简单的类型,合称 positional-or-keyword arguments 参数默认值 定义函数时,参数列表里可以为参数提供默认值,提供了默认值的参数是可选的,不具备默认值的参数是必选的。 因为可选参数有可能不提供,如果可选参数后面出现必选参数,就会出现混淆。 >>> def f(a,b=1,c): File "", line 1 def f(a,b=1,c): ^ SyntaxError: non-default argument follows default argument >>> 上面的例子里,如果调用函数 f(1, 2) 就难以确定 2 是应该被 b 还是 c 捕获。 所以得到一条原则: 原则2: 定义函数时,必选参数必须先于可选参数提供。 可变长度位置参数 有一种情形,是不能预知 位置参数 的数量,比如 max() 函数: >>> max('a') 'a' >>> max('a','b','c') 'c' >>> 使用 varargs 语法(*号,starred expression)将多个位置参数收集在一起,放入一个 parameter 中,这个 parameter 在函数内部作为一个 tuple 存在。 >>> def f(*a): ... print(a) ... >>> f(1,2,3,4) (1, 2, 3, 4) >>> 可以理解为:参数 a 前的星号将参数列表 (1,2,3,4) 收集在一起,合成一个 tuple, 赋值给变量a。这个过程叫 packing. 在定义函数时,参数名前的星号表示 packing;在调用函数时,参数名前的星号表示 unpacking: >>> def f(*a): ... print(a) ... >>> b='123' >>> f(*b) ('1', '2', '3') >>> 可变长度关键字参数 和 可变长度位置参数 类似,不能预知长度的 关键字参数 也可以用一个变量收集在一起,区别在于收集的结果不是 tuple 而是 dict, 而且使用两个星号。 >>> def f(**a): ... print(a) ... >>> f(b=1, c=2) {'b': 1, 'c': 2} >>> 一样可以 unpacking >>> f(**{'b':1,'c':2}) {'b': 1, 'c': 2} >>> 注意参数名在字典中是以字符串形式存在。这个例子有个额外的好处:有些情况下,变量名不能方便的写在函数参数列表里(比如参数名里有空格),解决方法就是用 unpacking: >>> def f(**a): ... for k,v in a.items(): ... print(k,v,sep='->') ... >>> f(**{'b d':1,'c':2}) b d->1 c->2 >>> 4种参数的两两组合 上面介绍了4种参数: 位置参数 关键字参数 可变长度位置参数 可变长度关键字参数 这4种参数存在4种两两组合场景,前两个参数的组合场景已经在 这一节 讨论。下面我们仔细观察另外3种场景下的参数定义。 普通位置参数 和 可变长度位置参数的组合 有这么一种场景,一些参数是固定的,另一些参数是不固定长度的,这样可以混合两种参数用法: 比如人的名字和携带的物品: >>> def f(name, *inventory): ... print(name+': '+', '.join(inventory)) ... >>> f('Amy','water','food') Amy: water, food >>> f('Bob','car') Bob: car >>> 由于 packing 是将右边所有位置参数都收集在一起,所以带星号的参数不能出现在在普通的位置参数之前,比如(*a, b),不然后面的参数就会被收集进去。 在 Python 2.x 的时代,(*a, b) 这么定义函数是不合法的: # Python 2.x >>> def f(*a, b): File "", line 1 def f(*a, b): ^ SyntaxError: invalid syntax >>> 在 Python 3.x 的时代,虽然允许了这种写法,但是*号表达式右边的参数必须以关键字形式提供,也就是(*a, b)中的b,这称为 Keyword-Only Arguments,另开一篇文章叙述。 普通位置参数 和 可变长度关键字参数的混合 类似上面一节的内容, 可以写出下面的函数定义: >>> def f(name, **inventory): ... print(name+': '+', '.join([k+': '+v for k,v in inventory.items()])) ... >>> f('Amy', food='bread', juice='apple') Amy: food: bread, juice: apple >>> f('Bob', car='jeep') Bob: car: jeep >>> 不允许普通位置参数写在 packing关键字参数后面: 将抛出 SyntaxError >>> def f(**inventory, name): File "", line 1 def f(**inventory, name): ^ SyntaxError: invalid syntax >>> 可变长度位置参数 和 可变长度关键字参数的混合 如果 位置参数数量和关键字参数数量都不能预知,那么就需要组合两种参数: >>> def f(*names, **inventory): ... print(', '.join(names)+': '+', '.join([k+': '+v for k,v in inventory.items()])) ... >>> f('Amy','Bob', car='jeep') Amy, Bob: car: jeep >>> f('Cindy', 'David',food='bread', juice='apple') Cindy, David: food: bread, juice: apple >>> 和 这一节相同的理由,可变长度位置参数 不能放在 可变长度关键字参数后面: >>> def f(**inventory, *names): File "", line 1 def f(**inventory, *names): ^ SyntaxError: invalid syntax >>> 结论 上面对 Python 函数的参数定义做了一番综述,概括下来有三类参数和三项原则: 三类参数 位置参数和 关键字参数,合称 positional-or-keyword arguments 可变长度位置参数 可变长度关键字参数 还有另外两种参数,在各自单独文章内介绍,分别是: Keyword-Only Arguments Positional-Only Arguments 合计 五类参数 三项原则 位置参数 调用函数时,位置参数必须先于关键字参数提供。 定义函数时,必选参数必须先于可选参数提供。 定义函数时,普通的位置参数必须先于可变长度参数提供。