Python函数参数进阶:关键字、可变参数与默认值陷阱
本文深入解析Python函数参数的进阶用法,涵盖位置参数与关键字参数的区别、使用*和**接收任意数量参数、keyword-only参数的强制规范作用,以及默认可变对象和None检测的常见陷阱与最佳实践。
1、关键字参数和位置参数
关键字参数(positional argument)和位置参数(keyword argument)
在Python中,函数的参数根据调用时传参的形式,分为关键字参数和位置参数。
(1)关键字参数
关键字参数是指在函数调用时,由标识符(如name=)引导的参数,或者放在一个由**引导的字典里进行传递。例如:
complex(real=3, imag=5)
complex(**{'real': 3, 'imag': 5})
(2)位置参数
非关键字参数的参数即为位置参数。它除了单独传递外,还可以放在一个由*引导的可迭代序列(如列表、元组等)里进行传递。例如:
complex(3, 5)
complex(*(3, 5))
位置参数总是位于函数参数列表的最前方,而关键字参数必须放在位置参数之后。它们的位置关系如下:
def func(arg1, arg2, kwarg1, kwarg2):
func(1, 2, kwarg1=3, kwarg2=4)
这里arg1、arg2是位置参数,kwarg1,kwarg2是关键字参数。关键字参数的key(即 'kwarg1=3'中的'kwarg1','kwarg2=4'中的'kwarg2')必须与形参名称一致。
2、接受任意数量的参数
(1)接受任意数量的位置参数
"*"号表达式除了用于对任意长度可迭代对象进行拆分外,还能在函数定义中使用,用于定义一个可以接受任意数量位置参数的函数。例如:
def avg(first, *rest):
print(rest)
return (first + sum(rest)) / (1 + len(rest))
print(avg(1, 2, 3, 4, 5))
# (2, 3, 4, 5)
# 1. 5
"*"开头的参数必须作为最后一个位置参数使用,且"*"开头的参数传进来后是元组数据结构。
(2)接受任意数量的关键字参数
想接受任意数量的关键字参数,我们可以类似地使用"**"开头的参数。例如:
import html
def make_element(name, value, **attrs) -> str:
key_values = [ ' %s="%s"' % item for item in attrs.items()]
attr_str = ''.join(key_values)
# Perform a string formatting operation.
element = '<{name} {attrs}>{value}</{name}>'.format(name=name, attrs=attr_str, value=html.escape(value))
return element
res_1 = make_element('item', 'Albatross', size='large', quantity=6)
res_2 = make_element('p', '<spam>') # escape会把这里'<spam>'中的'<'和'>'替代成安全的序列< >
print(res_1) # <item size="large" quantity="6">Albatross</item>
print(res_2) # <p ><spam></p>
"**"开头的参数必须作为最后一个关键字参数使用,且"**"开头的参数传进来后是字典数据结构。
(3)同时接受任意数量的位置参数和关键字参数
如果想要函数同时接受任意数量的位置参数和关键字参数,只要联合使用"*"和"**"即可。例如:
def anyargs(*args:tuple, **kwargs:dict):
print(args)
print(kwargs)
anyargs(2, 3, 4, 5, time=1, data=2)
# (2, 3, 4, 5)
# {'time': 1, 'data': 2}
3、keyword-only参数
前面说过, "*"打头的参数只能作为最后一个位置参数,"**"打头的参数只能作为最后一个关键字参数(自然也是最后一个参数),而依此推断"*"打头的参数后的参数就必然是关键字参数了。例如:
# 出现在*args之后的参数称为keyword-only参数
# 这两个例子中y都只能是关键字参数,在传参时要保证key和形参的一致性
def a(x, *args, y):
print(y)
def b(x, *args, y, **kwargs):
print(y)
a(4, 6, 7, 8, y=1)
b(4, 6, 7, 3, y=1, data=2, year=3)
# 1
# 1
这样的参数称为keyword-only参数,即出现在*args之后的参数只能作为关键字参数使用。
我们可以充分利用这一性质,将关键字参数放在以*打头的参数后,或者一个单独的*之后,强迫函数的调用者必须传关键字参数。例如:
def recv(max_size, *, block):
'Receives a message'
pass
recv(1024, True) # recv2() takes 1 positional argument but 2 were given
# and missing 1 required keyword-only argument: 'block'
recv(1024, block=True) # OK
这项技术在实际项目中,可以用来为接受任意数量的位置参数的函数来指定关键字参数。例如这个带截断功能的求最小值函数,这里的clip参数被强迫为必须按照关键字参数传入,而且设定了一个默认值None, 使参数为可选的。例如:
def mininum(*values, clip=None):
m = min(values)
if clip is not None:
m = clip if clip > m else m
return m
res1 = mininum(1, 5, 2, -5, 10)
res2 = mininum(1, 5, 2, -4, 10, clip=0)
print(res1, res2) # -5, 0
除此之外,keyword-only参数可以提高代码可读性。例如这种函数写法:
msg = recv(1024, False)
如果代码的阅读者不熟悉recv函数的工作方式,那么可能不太明白这里的False参数有什么作用。如果这个函数的调用可以写成下面这样的话,那就清晰多了(当然,需要这个函数的编写者最开始就强制函数的使用者这样写):
msg = recv(1024, block=False)
最后,如果函数定义的时候强制使用了keyword-only参数,那么当用户请求帮助信息时,参数信息可以很自然地显现出来:
print(help(recv))
# Help on function recv in module __main__:
# recv(max_size, *_, block)
# Receives a message
3、可选参数(带默认值的参数)
要想定义一个可选参数,需要在函数定义中为参数赋值,并保证默认参数出现在参数列表最后。例如:
def spam(a, b=42):
print(a, b)
spam(1) # 1, 42
spam(1, 2) # 1, 2
如果默认值是可变容器,比如说列表、集合、字典等,需要把None做为默认值。例如:
def spam(a, b=None):
if b is None:
b = []
警示1:千万不能直接像下面这样写:
def spam(a, b=[]):
如果像上面那样写,那么就会发生一些你所不期望看到的现象:如果默认值在函数体之外被修改了,那么这种修改在之后的函数调用中仍然阴魂不散。例如:
def spam(a, b=[]):
print(b)
return b
x = spam(1)
x.append('oh!')
x.append('no!')
print(x)
spam(1)
# []
# ['oh!', 'no!']
# ['oh!', 'no!']
警示2:在函数体中,我们常常需要判断参数是否为None,此处需要使用is运算符,千万不能直接像下面这样写:
def spam(a, b=None):
if not b:
b = []
这里的问题在于:尽管None会被判定为False,可还有其他许多对象(比如长度为0的字符串、列表、元组、字典等)也存在这样的行为。这样,有很多其他的特定输入也会被判定为False,然后本该是用户传进来的值直接被默认的[]覆盖掉了。例如:
def spam(a, b=None):
if not b:
b = []
spam(1) # OK
x = []
spam(1, x) # Oops! x will be overwritten by default []
spam(1, 0) # Oops! 0 will be overwritten by default []
spam(1, '') # Oops! '' will be overwritten by default []
最后,我们再来讨论一个非常棘手的问题。我们想要在函数中检测调用者是否对可选参数提供了某个特定值(可以是任意值,None也算)这样,我们自然就不能用None,0, False当做默认值然后再来做检测了,因为用户本身就可能拿它们当做参数。
要解决这个问题,可以像下面这样写:
_no_value = object()
def spam(a, b=_no_value):
if b == _no_value:
print("No b value supplied")
return
print(a, b)
spam(1) # No b value supplied
spam(1, 2) # 1 2
spam(1, None) # 1 None
这里_no_value是基于object()类创建的一个独有对象,可以用这个来对用户提供的参数做检测,因为用户几乎不可能把_no_value对象做为参数输入(除非用户传入的是相同的对象,否则哪怕是object类型的另一个对象都和_no_value对象是不同的)。
这里简要说明一下object类,object是Python中几乎所有对象的基类,object对象没有任何数据(底层缺少__dict__字典,甚至没办法设置任何属性),它唯一的作用就是用来检测相等性。
到此这篇关于关于Python函数参数的进阶用法的文章就介绍到这了,更多相关Python函数参数内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









