Python 的 函数参数处理机制

2022-01-05

本文综述 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)

这里的 ab 靠自身所处的位置捕获参数值,称为 Positional arguments. 这种通过位置对应获取参数的方式可以称为

  • 隐式(implicit)的获取
  • 匿名参数

参数值会按照位置一一对应到参数列表中的变量中去。如果提供的参数列表个数和定义的不符合,无论多还是少,都会抛出 TypeError

>>> def f(a,b):
...     pass
...
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'b'
>>> f(1,2,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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 "<stdin>", 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 "<stdin>", 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 "<stdin>", 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 "<stdin>", 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 "<stdin>", line 1
    def f(**inventory, *names):
                       ^
SyntaxError: invalid syntax
>>>

结论

上面对 Python 函数的参数定义做了一番综述,概括下来有三类参数和三项原则:

三类参数

还有另外两种参数,在各自单独文章内介绍,分别是:

合计 五类参数

三项原则

位置参数

调用函数时,位置参数必须先于关键字参数提供。
定义函数时,必选参数必须先于可选参数提供。
定义函数时,普通的位置参数必须先于可变长度参数提供。

捐助本站

为了保证阅读体验,本站不安放广告。但是,租用服务器和编写文章需要个人资金和时间的投入。

如果您觉得文章对您有用,请考虑捐助小站(金额不限),以期待更多原创文章。捐助记录

本站是个人网站,若无特别说明,文章均为原创,并采用 署名协议 CC-BY-NC 授权。
欢迎转载,惟请保留原文链接,且不得用于商业用途。