Python函数自变量的悠久历史

好吧,实际上,Python中参数的历史并不那么长。



我总是感到惊讶的是,要使用Python函数的参数,您只需要了解*args和即可**kwargs。我很惊讶没有白费。事实证明,争论远非易事。在这篇文章中,我想对Python中与函数参数有关的所有内容进行总体概述。我希望最终我能真正展示使用参数的一般情况,并且本文不会成为读者无法找到任何新东西的另一出版物。现在-到这一点。







我相信,本文的大多数读者都了解函数参数的本质。对于初学者,让我解释一下,这些是由调用者发送给函数的对象。在将参数传递给函数时,取决于要分派给函数的对象类型(可变或不可变对象),将执行许多操作。函数调用启动器是一个实体,它调用一个函数并将参数传递给它。说到调用函数,我们现在要讨论一些事情。



参数的名称在声明函数时指定,它们存储在调用时传递给函数的对象。此外,如果将某些内容分配给函数的相应局部变量(即它们的参数),则此操作不会影响传递给函数的不可变对象。例如:



def foo(a):
    a = a+5
    print(a)             #  15

a = 10
foo(a)
print(a)                 #  10


如您所见,函数调用丝毫不影响变量a将不可变对象传递给函数时,就会发生这种情况。



并且,如果将可变对象传递给函数,则您可能会遇到不同于上述行为的系统行为。



def foo(lst):
    lst = lst + ['new entry']
    print(lst)                #  ['Book', 'Pen', 'new entry']

lst = ['Book', 'Pen']
print(lst)                    #  ['Book', 'Pen']
foo(lst)
print(lst)                    #  ['Book', 'Pen']


您有没有注意到这里的新东西?如果您回答“否”,那您是对的。但是,如果我们以某种方式影响传递给该函数的可变对象的元素,我们将看到不同的东西。



def foo(lst):
    lst[1] = 'new entry'
    print(lst)                #  ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                     #  ['Book', 'Pen']
foo(lst)
print(lst)                     #  ['Book', 'new entry']


如您所见,lst在函数调用之后,来自参数的对象已更改。发生这种情况的原因是,我们正在使用对存储在参数中的对象的引用lst结果,更改此对象的内容超出了功能范围。您可以通过简单制作此类对象的深层副本并将它们写入函数的局部变量来避免这种情况。



def foo(lst):
    lst = lst[:]
    lst[1] = 'new entry'
    print(lst)                   #  ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                       #  ['Book', 'Pen']
foo(lst)
print(lst)                       #  ['Book', 'Pen']


那还没让你感到惊讶吗?如果不是这样,那么我想确保您跳过所知道的内容,并立即为您准备新的材料。如果是这样的话,请记住我的话,随着您更好地了解论点,您将学到很多有趣的东西。



因此,这是您对函数参数的了解:



  1. 位置参数传递给函数的顺序。
  2. 命名参数传递给函数的顺序。
  3. 分配默认参数值。
  4. 处理长度可变的参数集的组织。
  5. 解包参数。
  6. 使用只能通过名称传递的参数(仅关键字)。


让我们看一下这些要点。



1.将位置参数传递给函数的顺序



位置参数从左到右处理。也就是说,事实证明传递给函数的参数的位置与声明函数时在函数标头中使用的参数的位置直接对应。



def foo(d, e, f):
    print(d, e, f)

a, b, c = 1, 2, 3
foo(a, b, c)                  #  1, 2, 3
foo(b, a, c)                  #  2, 1, 3
foo(c, b, a)                  #  3, 2, 1


变量abc具有值1,2和3这些变量发挥与调用函数的参数的作用foo他们,在功能上的第一个呼叫,对应的参数def这种机制几乎适用于上述关于Python函数参数的6点知识。调用时传递给函数的位置参数的位置在为函数参数分配值中起着重要作用。



2.将命名参数传递给函数的顺序



命名参数将传递给函数,这些参数的名称与声明函数时分配给它们的名称相对应。



def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(a,b,c)                          #  1 2 3
foo(arg1=a, arg2=b, arg3=c)         #  1 2 3
foo(arg3=c, arg2=b, arg1=a)         #  1 2 3
foo(arg2=b, arg1=a, arg3=c)         #  1 2 3


如您所见,该函数foo接受3个参数。这些参数被命名为arg1arg2arg3调用函数时,请注意我们如何更改参数的位置。尽管系统继续从左到右读取命名实参,但将其与位置实参区别对待。在为函数参数分配适当的值时,Python会考虑参数的名称,而不是其位置。结果,无论传递给它的参数的位置如何,函数都会输出相同的东西。一直如此1 2 3



请注意,第1段中描述的机制在这里继续运行。



3.分配默认参数值



可以将默认值分配给命名参数。在函数中使用此机制时,某些参数将变为可选。此类函数的声明类似于我们在第2点中所考虑的。唯一的区别是这些函数的调用方式。



def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(arg1=a)                         #  1 0 0
foo(arg1=a, arg2=b )                #  1 2 0
foo(arg1=a, arg2=b, arg3=c)         #  1 2 3


请注意,在此示例中,我们并未按照函数声明中的描述将所有参数传递给该函数。在这些情况下,将为相应的参数分配默认值。让我们继续这个例子:



foo(arg2=b)                         #  0 2 0
foo(arg2=b, arg3=c )                #  0 2 3

foo(arg3=c)                         #  0 0 3
foo(arg3=c, arg1=a )                #  1 0 3


这些是使用上述机制通过将命名参数传递给函数来调用函数的简单易懂的示例。现在,通过结合我们到目前为止在第1,第2和第3点中讨论的内容,使我们的实验变得复杂



foo(a, arg2=b)                      #  1 2 0
foo(a, arg2=b, arg3=c)              #  1 2 3
foo(a, b, arg3=c)                   #  1 2 3

foo(a)                              #  1 0 0
foo(a,b)                            #  1 2 0


在此,调用函数时同时使用位置参数和命名参数。使用位置参数时,它们的指定顺序在正确将输入正确传递给函数中继续发挥关键作用。



在这里,我想提请您注意一个重要的细节。只是在命名参数之后不能指定位置参数。下面是一个示例,可以帮助您更好地理解这个想法:



foo(arg1=a, b)
>>>
foo(arg1=a, b)
           ^
SyntaxError: positional argument follows keyword argument
foo(a, arg2=b, c)
>>>
foo(a, arg2=b, c)
              ^
SyntaxError: positional argument follows keyword argument


您可以将其作为规则。调用函数时,位置参数不必遵循命名参数。



4.处理长度可变的参数集的组织



在这里,我们将讨论结构*args和结构**kwargs当在函数声明中使用这些构造时,我们希望在调用函数时,任意长度的参数集将表示为parameterargskwargs应用构造时*args,参数将args接收表示为元组的位置参数。当应用**kwargskwargs秋季命名的参数,列在一本字典。



def foo(*args):
    print(args)

a, b, c = 1, 2, 3

foo(a, b, c)                #  (1, 2, 3)
foo(a, b)                   #  (1, 2)
foo(a)                      #  (1)
foo(b, c)                   #  (2, 3)


此代码证明参数args存储了一个元组,该元组包含调用时传递给函数的内容。



def foo(**kwargs):
    print(kwargs)

foo(a=1, b=2, c=3)        #  {'a': 1, 'b': 2, 'c': 3}
foo(a=1, b=2)             #  {'a': 1, 'b': 2}
foo(a=1)                  #  {'a': 1}
foo(b=2, c=3)             #  {'b': 2, 'c': 3}


上面的代码显示参数kwargs存储了键-值对的字典,代表了被调用时传递给函数的命名参数。



但是,应注意,不能接受设计用于接受位置参数的函数命名参数(反之亦然)。



def foo(*args):
    print(args)

foo(a=1, b=2, c=3)
>>>
foo(a=1, b=2, c=3)
TypeError: foo() got an unexpected keyword argument 'a'
#########################################################
def foo(**kwargs):
    print(kwargs)

a, b, c = 1, 2, 3
foo(a, b, c)
>>>
TypeError: foo() takes 0 positional arguments but 3 were given


现在,我们将在点1、2、3和4中分析的所有内容放在一起,并进行所有这些实验,检查在调用它们时可以传递给函数的参数的不同组合。



def foo(*args,**kwargs):
    print(args, kwargs)

foo(a=1,)
# () {'a': 1}

foo(a=1, b=2, c=3)
# () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)
# (1, 2) {'a': 1, 'b': 2}

foo(1, 2)
# (1, 2) {}


如您所见,我们有一个元组args和一本字典可供我们使用kwargs



这是另一条规则。这是因为该结构*args不能在该结构之后使用**kwargs



def foo(**kwargs, *args):
    print(kwargs, args)
>>>
    def foo(**kwargs, *args):
                      ^
SyntaxError: invalid syntax


相同的规则适用于调用函数时指定参数的顺序。位置参数不能跟随命名参数。



foo(a=1, 1)
>>>
    foo(a=1, 1)
            ^
SyntaxError: positional argument follows keyword argument
foo(1, a=1, 2)
>>>
    foo(1, a=1, 2)
               ^
SyntaxError: positional argument follows keyword argument


当声明功能,您可以将位置参数,*args以及*kwagrs如下:



def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(1, a=1,)                            #  1
# 1 () {'a': 1}

foo(1, a=1, b=2, c=3)                   #  2
# 1 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)                     #  3
# 1 (2,) {'a': 1, 'b': 2}
foo(1, 2, 3, a=1, b=2)                  #  4
# 1 (2, 3) {'a': 1, 'b': 2}
foo(1, 2)                               #  5
# 1 (2,) {}


声明一个函数时,foo我们假定它必须有一个必需的位置参数。它后面是一组可变长度的位置自变量,在这之后是一组可变长度的命名自变量。知道这一点,我们可以轻松地“解密”上述每个函数调用。



1函数传递参数1a=1。它们分别是位置参​​数和命名参数。 2是多种多样的 1。此处,位置参数集的长度为零。



3我们传递函数12a=1,b=2。这意味着它现在接受两个位置参数和两个命名参数。根据函数声明,事实证明1作为一个需要位置参数,2进入一组的可变长度的位置参数,a=1b=2在一组可变长度命名参数结束。



为了正确调用此函数,我们必须至少传递一个位置参数给它。否则,我们将面临错误。



def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(a=1)
>>>
foo(a=1)
TypeError: foo() missing 1 required positional argument: 'var'


此函数的另一个变体是一个函数,该函数声明它接受一个必需的位置自变量和一个命名自变量,后跟可变长度的位置和命名自变量集。



def foo(var, kvar=0, *args,**kwargs):
    print(var, kvar, args, kwargs)

foo(1, a=1,)                               #  1
# 1 0 () {'a': 1}

foo(1, 2, a=1, b=2, c=3)                   #  2
# 1 0 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, 3, a=1, b=2)                     #  3
# 1 2 () {'a': 1, 'b': 2}

foo(1, 2, 3, 4, a=1, b=2)                  #  4
# 1 2 (3,) {'a': 1, 'b': 2}

foo(1, kvar=2)                             #  5
# 1 2 () {}


可以使用与分析前一个函数时相同的方式来“解密”该函数的调用。



调用此函数时,必须至少传递一个位置参数。否则,我们将遇到错误:



foo()
>>>
foo()
TypeError: foo() missing 1 required positional argument: 'var'
foo(1)
# 1 0 () {}


请注意,通话foo(1)正常。这里的要点是,如果在不为命名参数指定值的情况下调用函数,则该值将自动分配给它。



如果不正确地调用此函数,可能还会遇到一些其他错误:



foo(kvar=1)                             #  1
>>>
TypeError: foo() missing 1 required positional argument: 'var'
foo(kvar=1, 1, a=1)                      #  2
>>>
SyntaxError: positional argument follows keyword argument
foo(1, kvar=2, 3, a=2)                   #  3
>>>
SyntaxError: positional argument follows keyword argument


要特别注意运行时错误 3



5.解包参数



在前面的部分中,我们讨论了如何将传递给函数的参数集收集到元组和字典中。在这里,我们将讨论反向操作。即,我们将分析允许您解压缩提供给函数输入的参数的机制。



args = (1, 2, 3, 4)
print(*args)                  #  1 2 3 4
print(args)                   #  (1, 2, 3, 4)

kwargs = { 'a':1, 'b':2}
print(kwargs)                 #  {'a': 1, 'b': 2}
print(*kwargs)                #  a b


变量可以用语法进行拆包***这是将元组,列表和字典传递给函数时的用法。



def foo(a, b=0, *args, **kwargs):
    print(a, b, args, kwargs)

tup = (1, 2, 3, 4)
lst = [1, 2, 3, 4]
d = {'e':1, 'f':2, 'g':'3'}

foo(*tup)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(*lst)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(1, *tup)          # foo(1, 1, 2, 3, 4)
# 1 1 (2, 3, 4) {}

foo(1, 5, *tup)       # foo(1, 5, 1, 2, 3, 4)
# 1 5 (1, 2, 3, 4) {}

foo(1, *tup, **d)     # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 1 (2, 3, 4) {'e': 1, 'f': 2, 'g': '3'}

foo(*tup, **d)         # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 2 (3, 4) {'e': 1, 'f': 2, 'g': '3'}
d['b'] = 45
foo(2, **d)             # foo(1, e=1 ,f=2, g=3, b=45)
# 2 45 () {'e': 1, 'f': 2, 'g': '3'}


使用参数解压缩来解构此处显示的每个函数调用,并注意不使用*和时相应的调用的外观**尝试了解进行这些调用时发生的情况以及如何解压缩各种数据结构。



在尝试解压缩参数时,您可能会遇到一个新错误:



foo(1, *tup, b=5)
>>>
TypeError: foo() got multiple values for argument 'b'
foo(1, b=5, *tup)
>>>
TypeError: foo() got multiple values for argument 'b'


由于命名自变量b=5与位置自变量之间发生冲突,因此会发生此错误正如我们在第2节中发现的那样,当传递命名参数时,它们的顺序无关紧要。结果,在两种情况下都会发生相同的错误。



6.使用只能通过名称传递的参数(仅关键字)



在某些情况下,您需要使函数接受必需的命名参数。如果在声明函数时它们描述了只能按名称传递的参数,那么无论何时调用它,都必须将此类参数传递给它。



def foo(a, *args, b):
    print(a, args, b)

tup = (1, 2, 3, 4)

foo(*tup, b=35)
# 1 (2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, 5, *tup, b=35)
# 1 (5, 1, 2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, b=35)
# 1 () 35

foo(1, 2, b=35)
# 1 (2,) 35

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'b'

foo(1, 2, 3)
# TypeError: foo() missing 1 required keyword-only argument: 'b'


如您所见,可以预期该函数必定会传递一个命名实参b,该实参在函数声明中在之后指定*args在这种情况下,您可以在函数声明中仅使用一个符号*,其后用逗号分隔,其中包含命名参数的标识符,这些标识符只能通过名称传递给函数。这样的函数不会被设计为接受一组可变长度的位置参数。



def foo(a, *, b, c):
    print(a, b, c)

tup = (1, 2, 3, 4)

foo(1, b=35, c=55)
# 1 35 55

foo(c= 55, b=35, a=1)
# 1 35 55

foo(1, 2, 3)
# TypeError: foo() takes 1 positional argument but 3 were given

foo(*tup, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given

foo(1, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given


在上一个示例中声明的函数带有一个位置参数和两个命名参数,只能按名称传递。这导致以下事实:要正确调用一个函数,它需要传递两个已命名的参数。之后,*您还可以描述命名参数,并为其指定默认值。这在调用此类函数时给了我们一定程度的自由。



def foo(a, *, b=0, c, d=0):
    print(a, b, c, d)

foo(1, c=55)
# 1 0 55 0

foo(1, c=55, b=35)
# 1 35 55 0

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'c'


请注意该函数可以正常没有传递任何参数给它被调用,b而且d因为他们已经被赋予缺省值。



结果



也许我们确实有一个关于争论的很长的故事。我希望这些材料的读者对自己有所了解。顺便说一句,Python中的函数参数的故事还在继续。也许我们稍后再讨论。



您是否从此材料中学到了有关Python中函数参数的新知识?






All Articles