参数类型

阅读: 40630     评论:10

绝大多数函数接收一定数量的参数,然后根据实际调用时提供的参数的值的不同,输出不同的结果。前面我们说过,将函数内部的参数名字,定义得和外部变量的名字一样是一种不好的习惯,它容易混淆思维,甚至发生错误。通常我们定义和给函数传递参数是这样的:

x, y, z = 1, 2, 3

def add(a, b, c):

    return a+b+c

add(x, y, x)        # 使用变量,传递参数
add(4, 5, 6)        # 直接传递值也是可以的。

在上面的例子中,a,b,c叫做形式参数,简称形参。而x,y,z和4,5,6叫做实际参数,简称实参,也就是实际要传递的值。而我们通常讨论的参数,指的都是形参。

定义函数时,参数的名字和位置确定下来,函数的接口就固定了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。Python函数的参数定义灵活度非常大。除了正常定义的位置参数外,还可以使用默认参数、动态参数和关键字参数,这些都是形参的种类。

一、 位置参数

也叫必传参数,顺序参数,是最重要的,也是必须在调用函数时明确提供的参数!位置参数必须按先后顺序,一一对应,个数不多不少的传递!

上面例子中的a,b,c就是位置参数,我们在使用add(4, 5, 6)调用时,就是将4传给a,5传给b,6传给c的一一对应传递。类似add(4, 5, 6, 7)add(4)add(5, 4, 6)这种“画蛇添足”、“缺胳膊少腿”和“嫁错郎”类型的调用都是错误的。其中,add(5, 4, 6)的调用在语法上没问题,但是输出结果可能和预期的不一致。

注意: Python在做函数参数传递的时候不会对数据类型进行检查,理论上你传什么类型都可以!

def add(a, b, c):
    return a+b+c

result = add("haha", 2,  3)

但是,上面的add函数,如果你传递了一个字符串和两个数字,结果是弹出异常,因为字符串无法和数字相加。这就是Python的弱数据类型和动态语言的特点。在简单、方便的时候,需要你自己去实现数据类型检查。

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/func.py", line 33, in <module>
    result = add("haha", 2,  3)
  File "F:/Python/pycharm/201705/func.py", line 31, in add
    return a+b+c
TypeError: must be str, not int

二、 默认参数

在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。

def power(x, n = 2):
    return x**n

ret1 = power(10)   # 使用默认的参数值n=2
ret2 = power(10, 4)  # 将4传给n,实际计算10**4的值

上面例子中的n就是个默认参数。默认参数可以简化函数的调用,在为最常用的情况提供简便调用的同时,还可以在特殊情况时传递新的值。但是在设置默认参数时,有几点要注意:

  • 默认参数必须在位置参数后面!

如果你违反了这点,在语法层面直接是通不过的。

# 这是一个错误的例子
def power(n = 2,x):
    return x**n
  • 当有多个默认参数的时候,通常将更常用的放在前面,变化较少的放后面。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
    pass
  • 在调用函数的时候,尽量给实际参数提供默认参数名。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
    pass

student('jack','male',17)       # 其它全部使用默认值
student('tom','male',18,'102','666666','beijing')    # 全部指定默认参数的值
student('mary','female',18,'102',tel='666666')  # 挑着来
student('mary','female',18,tel='666666','beijing')   #  这是错误的参数传递方式
student("mary","female",18,tel="666666",address="beijing")

注意最后两种调用方式,倒数第二种是错误的,而最后一种是正确的。为什么会这样?因为一切没有提供参数名的实际参数,都会当做位置参数按顺序从参数列表的左边开头往右匹配!

  • 使用参数名传递参数

通常我们在调用函数时,位置参数都是按顺序先后传入,而且必须在默认参数前面。但如果在位置参数传递时,给实参指定位置参数的参数名,那么位置参数也可以不按顺序调用,例如:

def student(name, age, classroom, tel, address="..."):
    pass

student(classroom=101, name="Jack", tel=66666666, age=20)

注意指定的参数名必须和位置参数的名字一样。

  • 默认参数尽量指向不变的对象!

下面是国内某上市互联网公司Python面试真题:

def func(a=[]):
    a.append("A")
    return a

print(func())
print(func())
print(func())

不要上机测试,仅凭代码,你能说出打印的结果吗?

很多同学可能会说,这还不简单,肯定是下面的结果啊:

['A']
['A']
['A']

真的是这样吗?错了!真正的结果是:

['A']
['A', 'A']
['A', 'A', 'A']

Why?为什么会这样?

因为Python函数体在被读入内存的时候,默认参数a指向的空列表对象就会被创建,并放在内存里了。因为默认参数a本身也是一个变量,保存了指向对象[]的地址。每次调用该函数,往a指向的列表里添加一个A。a没有变,始终保存的是指向列表的地址,变的是列表内的数据!我们可以测试一下:

def func(a=[]):
    print("函数内部a的地址为:%s" % id(a))
    a.append("A")
    return a

b = func()
print('此时b的值为:%s' % b)
print("函数外部b的地址为:%s" % id(b))
print("-------------")

c = func()
print('此时c的值为:%s' % c)
print("函数外部c的地址为:%s" % id(c))
print("-------------")

d = func()
print('此时d的值为:%s' % d)
print("函数外部d的地址为:%s" % id(d))

打印结果是:

函数内部a的地址为:39287880
此时b的值为:['A']
函数外部b的地址为:39287880
-------------
函数内部a的地址为:39287880
此时c的值为:['A', 'A']
函数外部c的地址为:39287880
-------------
函数内部a的地址为:39287880
此时d的值为:['A', 'A', 'A']
函数外部d的地址为:39287880

那么如何避免这个问题呢?

使用不可变的数据类型作为默认值!

def func(a=None):
    # 注意下面的if语句
    if a is None:
        a = []
    a.append("A")
    return a

print(func())
print(func())
print(func())

将默认参数a设置为一个类似None,数字或字符串之类的不可变对象。在函数内部,将它转换为可变的类型,比如空列表。这样一来,不管调用多少次,运行结果都是['A']了。

三、 动态参数

顾名思义,动态参数就是传入的参数的个数是动态的,可以是1个、2个到任意个,还可以是0个。在不需要的时候,你完全可以忽略动态函数,不用给它传递任何值。

Python的动态参数有两种,分别是*args**kwargs,这里面的关键是一个和两个星号的区别,而不是argskwargs在名字上的区别,实际上你可以使用*any**whatever的方式。但就如self一样,默认大家都使用*args**kwargs

注意:动态参数,必须放在所有的位置参数和默认参数后面!

def func(name, age, sex='male', *args, **kwargs):
    pass

1.*args

一个星号表示接收任意个参数。调用时,会将实际参数打包成一个元组传入形式参数。如果参数是个列表,会将整个列表当做一个参数传入。例如:

def func(*args):
    for arg in args:
        print(arg)

func('a', 'b', 'c')

li = [1, 2, 3]
func(li)

运行结果是:

a
b
c
[1, 2, 3]

通过循环args,我们可以获得传递的每个参数。但是li这个列表,我们本意是让它内部的1,2,3分别当做参数传递进去,但实际情况是列表本身被当做一个整体给传递进去了。怎么办呢?使用一个星号!调用函数,传递实参时,在列表前面添加一个星号就可以达到目的了。实际情况是,不光列表,任何序列类型数据对象,比如字符串、元组都可以通过这种方式将内部元素逐一作为参数,传递给函数。而字典,则会将所有的key逐一传递进去。

def func(*args):
    for arg in args:
        print(arg)

li = [1, 2, 3]
func(*li)

2.**kwargs

两个星表示接受键值对的动态参数,数量任意。调用的时候会将实际参数打包成字典。例如:

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])
        print(type(kwg))

func(k1='v1', k2=[0, 1, 2])

运行结果是:

k1 v1
<class 'str'>
k2 [0, 1, 2]
<class 'str'>

而如果我们这样传递一个字典dic呢?我们希望字典内的键值对能够像上面一样被逐一传入。

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])

dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(dic)

实际结果却是弹出错误,为什么?

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/func.py", line 10, in <module>
    func(dic)
TypeError: func() takes 0 positional arguments but 1 was given

因为这时候,我们其实是把dic当做一个位置参数传递给了func函数。而func函数并不接收任何位置函数。那怎么办呢?使用两个星号

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])

dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(**dic)

有了前面一个星号的基础,这里我们应该很好理解了。两个星号能将字典内部的键值对逐一传入**kwargs

3.“万能”参数

*args**kwargs组合起来使用,理论上能接受任何形式和任意数量的参数,在很多代码中我们都能见到这种定义方式。需要注意的是,*args必须出现在**kwargs之前。

def func(*args, **kwargs):

    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = [1, 2, 3]
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(*lis, **dic)

现在我们结合一下普通参数和万能参数,看看会有什么情况发生:

def func(a, b, c=1, *args, **kwargs):
    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果是:

bbb
ccc
k1 v1
k2 v2

列表lis中的第一个元素‘aaa’怎么没有打印出来?

我们改一下代码,打印一下参数c的结果就知道了:

def func(a, b, c=1, *args, **kwargs):
    print('c的值是:', c)
    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果为:

c的值是: aaa
bbb
ccc
k1 v1
k2 v2

原来,lis的第一个元素被传递给参数c了!这就是Python的参数传递规则之一。

4.关键字参数

对于*args**kwargs参数,函数的调用者可以传入任意不受限制的参数。比如:

def func(*args):
    pass

func("haha", 1, [], {})
func(1,2,3,4,5,6)

对于这样的参数传递方式,虽然灵活性很大,但是风险也很大,可控性差,必须自己对参数进行过滤和判定。例如下面我只想要姓名、年龄和性别,就要自己写代码检查:

def student(name, age, **kwargs):
    if 'sex' in kwargs:
        student_sex = kwargs['sex']

但是实际上,用户任然可以随意调用函数,比如student("jack", 18, xxx='male'),并且不会有任何错误发生。而我们实际期望的是类似student("jack", 18, sex='male')的调用。那么如何实现这种想法呢?

可以用关键字参数!关键字参数前面需要一个特殊分隔符*和位置参数及默认参数分隔开来,*后面的参数被视为关键字参数。在函数调用时,关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。不同于默认参数,关键字参数必须传递,但是关键字参数也可以有缺省值,这时就可以不传递了,从而简化调用。

我们把前面的函数改写一下:

def student(name, age, *, sex):
    pass

student(name="jack", age=18, sex='male')

注意函数的定义体首行。

如果函数定义中已经有了一个*args参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了。

def student(name, age=10, *args, sex, classroom, **kwargs):
    pass

student(name="jack", age=18, sex='male', classroom="202", k1="v1")

Python的函数参数种类多样、形态多变,既可以实现简单的调用,又可以传入非常复杂的参数。需要我们多下功夫,多写实际代码,多做测试,逐步理清并熟练地使用参数。


 函数基础 变量作用域 

评论总数: 10


点击登录后方可评论

博主,小白在这不能理解,希望能够解释一下 “万能”参数 def func(a, b, c=1, *args, **kwargs): --》func(1, 2, *lis, **dic)中为何默认参数不能直接使用,而会被*list赋值呢?



Python就是这么设计的,很多语法是人为制定的规则。就好比我们为什么只能用def关键字来定义一个函数,而不能用func来定义函数。 也好比列表的索引为什么是0开始,而不是1开始的?实际上,以0开始有它的优点和历史原因,但不表示以1开始就不行,1也有1的好处。



*lis相当于传递了三个参数进去,第一个'aaa'把参数c的地址指向了aaa吧



“万能”参数 def func(a, b, c=1, *args, **kwargs): --》func(1, 2, *lis, **dic)如果想要使用默认参数必须得func(1, 2, 5,*lis, **dic) 不能使用func(1, 2, c=5, *lis, **dic),这点不解,求博主解答一下,万分感谢!



在文章的例子中已经说得很明白了, c=5这种参数传递形式会被当作是*args的一部分,体现了Python参数用法的复杂性。它不是自然界中1+1=2这种显而易见,或者在任何一个学科领域都通用的定理,没有什么为什么,Python就是这么规定的。其具体规则都是由Python官方参照编程语言的通用做法制定的,如果你理解不了,那就先背下来,不用错就行。



def func(**kwargs): for kwg in kwargs: print(kwg, kwargs[kwg]) print(type(kwg)) func(k1='v1', k2=[0, 1, 2]) ,博主循环中的kwg的类型为什么是str呢?



这是Python的参数规则,所有 key=value方式提供的参数,默认情况下,key会以字符串的类型供使用。



传递实参的原则: * 位置实参必须在关键字实参的前面。 * 位置实参到形参的传递,按照从左到右的顺序,依次传递。 * 已经被位置实参赋值的形参,不可以再次使用关键字实参进行赋值,否则会报错。 * 调用函数时,传递的关键字参数会转化为位置参数,然后通过形参名称,实现形参(名称)与实参(对象)的绑定。参考:If keyword arguments are present, they are first converted to positional arguments, as follows. First, a list of unfilled slots is created for the formal parameters. If there are N positional arguments, they are placed in the first N slots. Next, for each keyword argument, the identifier is used to determine the corresponding slot (if the identifier is the same as the first formal parameter name, the first slot is used, and so on). If the slot is already filled, a TypeError exception is raised. Otherwise, the value of the argument is placed in the slot, filling it (even if the expression is None, it fills the slot). When all arguments have been processed, the slots that are still unfilled are filled with the corresponding default value from the function definition. (Default values are calculated, once, when the function is defined; thus, a mutable object such as a list or dictionary used as default value will be shared by all calls that don’t specify an argument value for the corresponding slot; this should usually be avoided.) If there are any unfilled slots for which no default value is specified, a TypeError exception is raised. Otherwise, the list of filled slots is used as the argument list for the call. * 传递的实参数目一定要与定义的形参数目匹配。 * 不匹配情况之一:某些形参被给与默认值,传入的参数的数目可以小于形参。 * 不匹配情况之二:有可以接受任意个数的形参,比如*parameter或者**paramater(本质上也算是匹配) * 总之:在函数调用时,形参一定要被传参,不管是来自于 位置实参,关键字实参还是默认值。参考:A function call always assigns values to all parameters mentioned in the parameter list, either from position arguments, from keyword arguments, or from default values.



背景知识: * Names(Identifiers): 名称/标识符,是对象的引用/别名。 * 常见的名称/标识符有:变量名,函数名,类名,模块名,函数的形参等。 * 名称绑定(Banding of Names):当被python解释器执行时,名称必须是绑定到对象的,不能单独存在。而被名称绑定的对象,如果在内存中不存在,就会被创建。 * 注释: * 解释器执行到函数的定义时,首行def定义的函数名会被evaluate成一个函数体,圆括号中被赋默认值的形参,会实现形参(名称)到默认值(对象)的绑定(默认值如果在内存中不存在,会被创建),而那些并没有被赋默认值的形参,并不会被evaluate。 * 函数体(func-body)中的语句,只有在函数被调用时,才会被解释器执行。 * 名称绑定到对象发生在以下几种情形: * 对变量进行赋值操作(使用赋值运算符 = ),变量名绑定到对象。 * 类定义,类名绑定到类对象。 * 函数定义:函数名绑定到函数对象。 * 在交互式模式下,函数定义完成后,函数对象在内存中建立,当前的符号表中,函数名也绑定到了函数对象。不过此时函数体中的语句并没有执行,只有调用函数时,函数体中的语句才会执行。 * 在脚本模式下,代码执行完函数定义后,函数对象在内存中建立,函数名也绑定到了函数对象。不过此时函数体中的语句并没有执行,只有调用函数时,函数体中的语句才会执行。 * 函数定义:形参被赋默认值时。被赋默认值的形参到默认值的绑定发生在解释器执行到函数定义首行时,而且值的绑定只处理一次,随后对函数对象的调用时,如果没有明确再传入新的值,那么并不会再次对已经给与默认值的形参进行再次绑定。参考:Default parameter values are evaluated from left to right when the function definition is executed. * 函数调用:除了被赋默认值以外的形参到实参的绑定发生在函数调用时! * import语句,模块名绑定到模块对象。 * for循环(for i in iterables:), 变量名i在每一次循环中都会绑定到可迭代对象中的元素。 * as操作:不熟,后期再添加。 函数的参数: * 形参(parameters/formal parameters): 函数定义时,函数名后括号中定义的参数(名称) * 实参(arguments/actual parameters):函数调用时,传递给函数(对象)的参数(对象),而参数的传递,本质是将形参(名称)绑定到实参(对象),这个名称绑定的实现方式是:解释器将实参(对象)的内存地址传入函数对象,这样形参就可以根据内存地址找到对象,进行绑定。 * 所以,简单来说,形参是Name, 而实参是Object。函数调用时的传递参数,即实现 形参到实参的绑定。 * 传递的实参也可以是一个变量名,这样的话,本质是 形参绑定到 变量绑定的对象,仍然是形参绑定到对象。 函数参数的分类: * 形参定义格式分类: * 普通形参: * 格式(Form):parameter * 可接受的实参传递的方式: * 一个位置实参 * 或者 一个关键字实参 * 被赋默认值的形参: * 格式:parameter = default_value * 可接受的传递实参的方式: * 零个实参,也就是可以不传参,此时使用默认值 * 或者 一个位置实参 * 或者 一个关键字实参 * 有单个星标前缀的形参: * 格式:*parameter * 可接受的传递实参的方式: * 任意多个位置实参(包括零个) * 有两个星标前缀的形参: * 格式:**parameter * 可接受的传递实参的方式: * 任意多个 关键字实参(包括零个) * 传递实参格式分类: * 关键字实参(keyword arguments): * 通过指明形参名称传递实参: * 格式:key_arg=value: * 样例:complex(real=3, imag=5),其中3和5是关键字实参 * 通过字典对象传递实参: * 格式:**dict_object,其中dict_object中,key是形参名称,value是实参值(对象)。 * 样例:complex(**{'real': 3, 'imag': 5}),其中,real,imag是形参名称,3和5是实参。 * 位置实参(positional arguments): * 未指明形参名称的实参: * 格式:argument * 样例:complex(3, 5),其中3和5是位置实参。 * 通过可迭代对象传递实参: * 格式:*iterable,其中iterable的元素就是实参值(对象) * 样例:complex(*(3, 5)),其中元组(3,5)的元素3和5就是位置实参。



这个特性是py3的,我用的是py2...