Python参数体系的完整介绍

转资:

Python 为什么会有命名关键字参数? - 知乎
https://www.zhihu.com/question/57726430

千字预警!接下来的内容试图以最准确、同时尽可能清晰的语言,将Python函数定义时的形式参数,以及函数调用(使用)时的实际参数,其诸多形式分别介绍清楚,乍一看未必好理解,但完全值得反复阅读几次,到时定能给你茅塞顿开之感。

本文假设你了解Python中元组(tuple)字典(dict)等基本数据类型,并且知道函数定义

# 函数定义,独立成块的代码,圆括号内是形式参数
def funcname(形参表):
    函数体

和函数调用的基本形式

# 函数调用,可存在于任意表达式中,圆括号内是实际参数
funcname(实参表)

目录:

  • 预备知识(已了解函数基本特征的可跳过)
  • 函数调用
  • 函数定义
  • 函数调用的补充说明
  • 形参默认值

预备知识

何为函数?

函数是一段具有特定功能、与主程序隔离开来、易于复用的程序代码。注意“隔离”这个特征,即其内部新定义的变量均应视为其私有的,函数最好只通过参数表和返回值跟外部交换数据。

何为函数调用?

函数调用是指
将一组特定数据传递给该函数(传递参数)、
然后启动该函数体的执行、
最后从该函数返回到主程序(相对于被调用的函数而言的,主程序本身也可能是一个函数)中的调用点并带回特定值(返回值)
的过程。

这就给我们提出了两个要求

  1. 准确地定义该函数,尤其是它的形参表,这规定了我们需要向该函数传递一些什么样的数据
  2. 正确地调用该函数,也就是将我们的实际数据按照正确的形式传递过去,不匹配的行为就会造成错误,可能是解释器报错,也可能是与你意图不符算出错误结果。

请务必清晰区分形式参数(形参)和实际参数(实参),两者的使用规则相互匹配但并不一致,下面将分几节分别介绍,请务必注意区分。

函数调用

为啥要先讲调用?逻辑上函数当然是先定义好才能调用,但事实上,定义的形式是按照调用的需求来的,所以先了解调用形式有利于理解各种定义的缘由。而且调用的形式也比定义的形式要简单和容易理解

Python函数调用时,实参表由左到右就是简单的两个部分

funcname(【位置实参】,【关键字实参】)

注意这个前后顺序是严格的,两个部分都可以缺省,但不能相互交错!请在阅读下面(尤其是函数定义的部分)时牢牢记住这一点。

位置实参就是由逗号,分隔、按照前后顺序摆放的诸多实际参数,它们按照位置匹配函数定义时的形式参数,以函数func_1为例,

# a, b, c, d就是它的形式参数
def func_1(a, b, c, d):
    pass

若按如下方式进行调用

# x, y, z, w就是它的实际参数,可以跟上面的形参重名
func_1(x, y, z, w)

在函数体执行前就会形成

a = x
b = y
c = z
d = w

的传参效果。

关键字实参同样由逗号分隔,区别在于要在实参前加上形参名=这样的前缀,从而实现了一种显式的参数匹配效果,可以摆脱位置的约束。比如还是上面的func_1,我们这次完全使用关键字实参来调用,

func_1(b=y, a=x, d=w, c=z)  # 这种形式可以无视位置

当然,单纯的摆脱位置约束还不足以体现关键字参数的优势,结合有意义的形参名和合理设定的默认值,关键字参数可以拥有很好的可读性和易用性

例如pandas库中的一个常用函数 read_excel

形参数目多达二十多个,绝大部分形参名称都有明显含义,且有默认值。这时 若要求所有实际参数都按特定位置码放,对调用者和阅读者来说都是很大负担,且不能略过中间已有默认值的参数。这时关键字参数的威力就发挥出来了。比如我可以这样调用它
# 每个参数等号左边是形参,右边是实参
pandas.read_excel(io=my_file_path, index_col=0, sheet_name=1)
这就会在函数体执行前形成
io = my_file_path
index_col = 0
sheet_name = 1
的传参效果。 为了避免逻辑的混乱,我们将在最后讲解默认值的设定,之前我们始终假定形参都没有默认值。

当同时使用两种形式的参数时,就是上面的从左到右两个部分,依然是上面的func_1,这回使用混合调用形式,

func1(x, y, d=w, c=z)

这时你大概可以理解为啥位置实参和关键字实参这两种形式要有严格的前后顺序(尽管关键字实参内部可以无序)了——这确定了位置匹配的唯一性

必须指出,参数传递允许两种形式,但一个没有默认值的形参必须且只能被匹配一次,不能在位置上匹配一次又在关键字匹配一次。两种实际参数要共同覆盖所有没有默认值的形式参数

同时,我们也可以清楚地看到一些问题的存在。这种最简形式的func_1定义给了调用者充分的自由,但却缺少了必要的约束,比如没有强制要求前面几个参数必须按位置传递,或者后面几个参数必须按关键字传递。另外,在不设默认值的情况下,我们无法接收变长的实参表(包括变长的位置参数,以及不限名字和数量的关键字参数)。这是函数定义要解决的问题。

函数定义

这里重点讨论的是形参表,其他需要注意的主要是函数名 funcname 与当前模块的所有变量、函数共享命名空间,不要跟已有变量、函数重名就好;函数体的函数代码与主程序是隔离的,拥有独立的命名空间。

函数定义时,所有形参也由逗号,分隔,形式上就是一组普通变量,这些变量用于接收函数调用时传入的具体数据,其作用域为该函数局部,与主程序不冲突。完整地说,它从左到右可以分为五个部分

def funcname(【限定位置形参】,【普通形参】,【特殊形参args】,【限定关键字形参】,【特殊形参kwargs】):
    pass

按最简形式定义出来的就是【普通形参】,它们是“位置、关键字兼容”的,也就像上面展示的那样,其他部分是【普通形参】的扩展,满足了函数调用的特殊需求,下面逐一介绍。其中,两个特殊形参分别只能是0个或1个,其他形参可以是0个或多个。这个顺序同样是严格的,不同部分不能交错放置。

限定位置形参(Python 3.8正式引入)

纯位置形参,是为了限制开头几个参数只能按位置传递。Python从3.7开始,为某些内置函数定义了这种positional-only的形参,譬如abs函数(求绝对值的)

从它的参数提示你可以清晰地看出x是positional-only的,也就是说你不能通过abs(x=some_value)来调用它,毕竟这种参数名x并无明显含义,强用关键字形式并无好处

从Python 3.8开始,positional-only形参将可正式用于自定义函数中,它们必须放在形参表的最前面,并在后面使用斜杠/(独占一个参数位)与普通形参分隔,比如下面这样

# a, b, c成为限定位置形参
def func_2(a, b, c, /, d):
    pass

这时func_2的形参a, b, c将只能按位置接收实际参数,d仍是普通形参,可以兼容两种形式。

限定关键字形参(常叫命名关键字参数)

限定关键字形参,当然就是为了限制后面几个参数只能按关键字传递,这往往是因为后面几个形参名具有十分明显的含义,显式写出有利于可读性;或者后面几个形参随着版本更迭很可能发生变化,强制关键字形式有利于保证跨版本兼容性

限定关键字形参(keyword-only),限制调用者不能按位置传递,需要放在形参表的后面,并在前面使用星号*(独占一个参数位)与普通形参分隔,即类似这样

def func_3(其他形参, *, kw1, kw2):
    pass

这时参数kw1, kw2在函数调用时必须显式写出,即类似func_3(其他实参, kw1=var1, kw2=var2)的形式。

下面举一个三种混合形参的例子

def func_4(a, b, c, /, m, n, *, kw1, kw2):
    pass

其中的m, n当然就是兼容两种形式的普通形参。

两个特殊形参

两个特殊的形参位于限定关键字形参的前后,前者紧随星号*跟它占同一位置,后者则独占最后一个参数位,并使用双星号**前缀。

前者常常取名为args(当然你取别的名也无所谓),比如

def func_5(m, n, *args, kw1, kw2):
    pass

这允许args接收调用时把所有限定位置形参和普通形参都匹配完后剩余的位置实参,并封装成一个元组,你可以在函数内部通过args这个变量名使用它。如果它未接收到值则成为空元组。注意它位于普通形参之后,又只能接受位置实参,所以调用时如果希望它接收到值,前面的普通参数将也只能按位置传递;如果忽略它,普通形参倒依然能兼容关键字形式。

最后一个双星号特殊形参常常取名为kwargs(也可以取别的名字),即

def func_6(m, n, *, kw1, kw2, **kwargs):
    pass

允许kwargs接收所有在调用时未成功匹配的关键字参数,并封装成一个字典。形参名变成字符串形式的键,实参成为相应键对应的值,你可以在函数内部通过kwarg这个变量使用它。不过注意由于这里面的键都是前面不存在的形参名,自由度甚高,调用者很容易不小心把前面的形参名打错,导致其被错误地传进kwargs,设计者使用它时务必谨慎。

函数调用的补充说明

再次强调,尽管函数定义的形式如此丰富,调用形式永远是之前提到的简单的前后两部分——位置实参+关键字实参。这里明确下参数传递的基本规则。

与有无默认值无关,位置实参永远按位置传递给**args之前对应的形参(即限定位置形参和普通形参),多余的位置实参传入*args(如果有的话),关键字实参则匹配剩下的普通形参和限定关键字形参(非限定位置形参),多余的关键字实参则传入**kwargs(如果存在的话)。

以及

  1. 没有*args时,位置实参不能多于限定位置形参和普通形参的总量;
  2. 没有**kwargs时,关键字参数必须在普通形参和限定关键字形参中存在;
  3. *args**kwargs外所有没有默认值的形参都必须匹配到值。
  4. 同一形参不能被匹配两次。

大多数代码编辑器,都可以为你提示出函数定义时的形参表,请确认你传入的实际参数满足条件,那么至少语法上就没问题了。出问题请自行对照以上规则进行检查。

特殊传参方法

序列解包

当你有个序列对象,想将其中元素解放出来作为调用函数的位置实参时,给它加个前缀*即可,例如你有个两个列表lst1和lst2,

lst1 = [0, 2, 1]
lst2 = [3, 5, 6]

调用如下的函数func_7,比如这样

# 这样定义
def func_7(a, b, c, /, m, n, *args, kw1, kw2):
    pass

# 这样调用
func_7(*lst1, x, *lst2, kw1=y, kw2=z)

则形成了

a = lst1[0]
b = lst1[1]
c = lst2[2]
m = x
n = lst2[0]
args = (lst2[1], lst[2])
kw1=y
kw2=z

的传参效果。

注意这看起来跟定义时的*args是互逆的操作,但其实它们有很大不同——序列解包是位置实参的一部分,可以出现多次,也不限定具体位置,只要最终等效的实参表满足上面的匹配规则即可。

字典解包

当你有个字典对象,且其中的键都是合法的形参名时,你可能会想把其中的键值对解放出来作为调用函数的关键字参数,这时给它加个前缀**即可, 例如你有个两个字典dct1和dct2,

dct1 = {'kw1': 0, 'kw2': 2}
dct2 = {'n': 3, 'kw3': 5, 'kw4': 6}

调用如下的函数func_8,比如这样

# 这样定义
def func_8(a, b, c, /, m, n, *, kw1, kw2, kw3, **kwargs):
    pass

# 这样调用
func_8(1, 2, 3, 4, **dct1, **dct2)

则形成了

a = 1
b = 2
c = 3
m = 4
n = dct2['n']
kw1 = dct1['kw1']
kw2 = dct1['kw2']
kw3 = dct2['kw3']
kwargs = {'kw4': dct2['kw4']}

的传参效果。

类似地,它看起来跟定义时的**kwargs是互逆的操作,但同样有很大不同——字典解包是关键字实参的一部分,可以出现多次,也不限定具体位置,只要最终等效的实参表满足上面的匹配规则即可。

形参默认值

以上章节均假设所有形参没有默认值,是为了更清晰地梳理参数匹配的关系。首先明确一点,默认值是设给形参的;其次,默认值的使用并不受限于形参究竟是位置的还是关键字的,所以像这样将它介绍为一套独立规则是合理的。尽管最常见的编码行为是为关键字形参设定默认值,但作为一套完整的规则介绍,这里必须指出这并不是一定的(竟然写出了点翻译腔……)。

默认值的设定规则极其简单

  1. 两个特殊形参*args**kwargs不能设定默认值(或者你可以理解为它们默认值就是空元组和空字典) ;
  2. 默认值可以从限定位置形参或普通形参中的任意一个开始设定,这时须将后面剩下的所有限定位置形参和普通形参覆盖完;限定关键字形参的默认值则可以随意设定,无需考虑顺序问题。也就是说在遵循上面的形参规则的前提下,除了限定关键字形参,所有带默认值的形参必须位于无默认值的形参之后。
  3. 建议为所有限定关键字形参都设上默认值

比如下面各种形参类型最完整的func_9,可以从a, b, c, d, m, n, kw1, kw2, kw3中的任意一个开始设定默认值,直到最后。

def func_9(a, b, c, /, d, m, n, *args, kw1, kw2, kw3, **kwargs):
    pass

比如

# 从限定位置形参开始设定
def func_9(a, b, c=0, /, d=1, m=2, n=3, *args, kw1=4, kw2=5, kw3=6, **kwargs):
    pass

或者

# 从某个普通形参开始设定,这是最常见的做法
def func_9(a, b, c, /, d, m=0, n=1, *args, kw1=2, kw2=3, kw3=4, **kwargs):
    pass

又或者

# 从限定关键字形参开始设定
def func_9(a, b, c, /, d, m, n, *args, kw1=0, kw2=1, kw3,=2 **kwargs):
    pass

显然,默认值最大的作用就是允许调用者适当地忽略一些形参,但是注意,我们有必要第三次强调,调用形式还是得遵循前后两部分——位置实参+关键字实参、前后不交错的原则。由于必须匹配到所有无默认值的形参,位置实参又不具有可跳跃性,所以一般建议至早从最后一个限定位置形参开始设默认值,这样可以允许调用者自由地使用关键字参数来匹配需要传递非默认值的参数

重要的补充说明:

  • 强烈建议使用不可变对象,如整数浮点数字符串TrueFalseNone以上类型组成的元组等设定默认值,因为默认值只会在函数定义时被设定一次,如果是可变对象,一旦在函数内部被原地修改,效果会保留至以后每次的函数调用,不会被重新初始化。
  • 如果非要使用某个可变对象作为默认值,比如列表,或者要设定依赖于其他参数的默认值,建议设成None,然后写成类似这样的代码
def func_10(x, default=None):
    if default is None:
        # 这里可以书写更复杂的初始化行为
        default = []
    # 剩余的函数体
  • 有默认值的形参名,最好具有明显的含义,容易让人记住。

目录

预备知识

函数调用

函数定义

限定位置形参(Python 3.8正式引入)

限定关键字形参(常叫命名关键字参数)

两个特殊形参

函数调用的补充说明

特殊传参方法

形参默认值


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值