【摘要】
这是自动化运维新手村中除了数据结构番外篇之外的另一个番外,这个番外主要给大家详细讲解一下Python中的一些特殊技巧,这些技巧在平时写代码的过程中会极大的帮助我们快速的解决问题,所以你想成为一个真正的Pythoner,想让自己的代码更Pythonic,一定要仔细阅读这个番外!
在之前的文章中我们偶尔有使用到*args
作为函数参数,与之对应的还有**kwargs
,这对于刚接触Python的朋友会有点儿晕,这篇文章我就一次性将Python中的参数讲解清楚。
Python的参数可分为必传参数(必填)、默认参数(有默认值的非必填)、可变参数,这三种参数适用的场景不同,主要区别在于实参与形参的映射关系。
形参:
实参:
本文以实现一个学生的个人情况函数为例。
【必传参数】
通俗来说,必传参数就是必须传入的参数,其特点就是必填。定义student()
如下:
def student(name, age):
print("My name is ", name)
print("I am %s years old." % age)
我们可以通过调用student()
函数来观察一下必传参数有什么特点
student("ethan", 18) # 输出 My name is ethan \n Iam 18 years old.
student(18, "ethan") # 输出 My name is 18 \n Iam ethan years old.
- 一般来说,必传参数会按照函数定义时的顺序传入,即按位置进行映射。
student(name="ethan", age=18) # 输出 My name is ethan \n Iam 18 years old.
student(age=18, name="ethan") # 输出 My name is ethan \n Iam 18 years old.
- 如果指定了所有必传参数的名称,则可以不按顺序传入,即按名称进行映射。
【默认参数】
此时有一个新需求,需要给学生的age
设置默认值为18,即
- 不传
age
时,age
为默认值(18)。 - 传
age
为18时,age
为传入值。
这里,使用默认参数(又叫关键字参数,非必填且有默认值),其表现形式为在形参后接 =默认值
。
def student(name, age=18):
print("My name is ", name)
print("I am %s years old." % age)
student("ethan") # 输出 My name is ethan \n Iam 18 years old.
student("ethan", age=20) # 输出 My name is ethan \n Iam 20 years old.
上述指定了默认的age
参数为18。
我们下面分为仅定义默认参数时、同时定义必传参数和默认参数时两种情况来讨论默认参数的具体特点。
一、仅定义默认参数时
def student(name="ethan", age=18):
print("My name is ", name)
print("I am %s years old." % age)
student("ethan") # 输出 My name is ethan \n Iam 18 years old.
student(age=20, name="ethan") # 输出 My name is ethan \n Iam 18 years old.
1.1 函数调用时,默认参数的映射规则与必传参数相似,即:
- 一般情况下按位置;
- 指定全部名称情况下按名称。
二、同时定义必传参数和默认参数时
2.1 定义函数
def student(name, city="HZ", nation="CN", age):
pass
SyntaxError: non-default argument follows default argument
定义函数时,必传参数不可以定义在默认参数后面。
2.2 调用函数时,以下定义一个student()
函数来讨论。
def student(name, age, city="HZ", nation="CN"): # name,age为必传,city,nation是默认
print("My name is ", name)
print("I am %s years old." % age)
print("I am located at %s, %s" % (city, nation))
其中:
name
,age
为必传参数,city
,nation
为默认参数。
student("ethan", nation="SG", 18, city="BJ") # SyntaxError: positional argument follows keyword argument
student("ethan", nation="SG", age=18, city="BJ") # 输出 name=ethan age=18 city=BJ nation=SG
1. 一般情况下,必传参数必须出现在默认参数前面,否则映射关系不明确,会报错。
可以这么理解,若默认参数出现在必传参数前面,那第一个参数到底是有传入的默认参数还是在不传默认参数的情况下的必传参数呢?
student(name="ethan", nation="SG", age=18, city="BJ") # 输出 name=ethan age=18 city=BJ nation=SG
2. 如果指定了所有必传参数的名称,则可以不考虑传入的顺序。
【可变参数】
现在新增一个需求,需要计算学生的学科总分,并且学科的数量不定,最多3个。
这个需求可以通过默认参数来实现,设置3个默认参数,其默认值为None
,在函数内部进行判断该值是否传入。
def student(name, age, subject_1=None, subject_2=None, subject_2=None): # subject_*为穷举出来的学科分数
sumScore = 0 ## 总分
if (subject_1) {
sumScore += subject_1
}
if (subject_2) {
sumScore += subject_2
}
if (subject_3) {
sumScore += subject_3
}
print("My name is ", name)
print("My sum Score is " % sumScore)
- 在不定参数有限可穷举的情况下,才能通过默认参数来实现,且代码冗余较多。
- 但是如果不定参数数量不可估算,则默认参数无法实现。
因此,为了解决这一问题,Python引入了可变参数(非必传且传入个数可变),在之前的文章中我们偶尔有使用到*args
、**kwargs
作为函数参数,这两个都属于可变参数。
可变参数使用到了Python中的一个语法特性:打包和解包
打包:把多个值打包成一个元组。
解包:把一个元组或数组解析成多个值。
一、打包和解包
关于打包和解包在很多语言中都支持这个特性,但使用的语法略有不同,下面通过几个例子了解一下Python中的打包解包如何使用。
scores = 80, 90, 98 # 打包语法
chinese, math, english = val # 解包语法
print(chinese, math, english) # 输出 80, 90, 98
chinese, math, english = scores # ValueError: not enough values to unpack (expected 4, got 3)
-
在其他编程语言中,不会出现一个等号左边多个变量而右边只有一个变量的语法,这种写法是Python中特有的解包语法。
-
被解包的序列中的元素数量必须与赋值符号
=
左边元素的数量完全一样。
scores = 80, 90, 98, 100 # 打包语法
a, b = scores # ValueError: too many values to unpack (expected 2)
a, *b, c = scores # 打包和解包语法一起使用??????????????????这里的*不解释一下吗
print(b) # [90, 98]
print(c) # 4
- 可以在等号左边使用打包语法,将等号右边变量的多个值打包赋值给变量b,此时b为列表类型的变量。
scores = [80, 90]
chinese, math = *scores # SyntaxError: can't use starred expression here
- 不能将
*
操作符用于表达式的右边,这是需要特别注意的,如果要解包,直接使用a, b = val
即可。
def student(name, age, city, nation):
print("My name is ", name)
print("I am %s years old." % age)
print("I am located at %s, %s" % (city, nation))
student(*["ethan", 18, "BJ", "CN"]) # 输出 My name is ethan \n Iam 18 years old. I am located at HZ, CN.
student(*["ethan", 18, "BJ"]) # TypeError: student() missing 1 required positional argument: 'nation'
-
解包同样可以运用于函数传参中,上面例子就是将数组解包传入函数中,分别对应
a, b, c
三个形参。 -
解包后的数量必须与函数定义的形参数量相同。
如果我们不知道具体会传入多少个参数,这时候就要*args
上场了
二、*args
*args
属于参数中的可变参数,因为它并没有指定关键字,而是表示诸多参数的合并。
这里函数定义时的*
其实就是起到打包的作用,函数定义时的*args
可以表示任意个数的参数
def student(name, *scores):
print("My name is ", name)
print("My scores are ", *scores)
print("My avg score is ", sum(scores))
student("ethan")
# 输出 My name is ethan
student("ethan", 80)
# 输出 My name is ethan \n My scores are 80 \n My sum score is 80
student("ethan", 80, 90, 98)
# 输出 My name is ethan \n My scores are 80 90 98 \n My sum score is 268
根据上面的实例可以观察到以下几点:
-
*args
可以将多个参数打包成一个元组。 -
*args
是可变参数,可以不传,也可以传任意多个。
def student(*scores, name):
print("My name is ", name)
print("My scores are ", *scores)
student(80, 90, "ethan") # 输出 TypeError: foo() missing 1 required keyword-only argument: 'a'
student(80, 90, name="ethan") # 输出 My name is ethan \n My scores are 80 90
-
args
只是通俗约定的名称,实际可以叫任何其他的名称,例如本例中就叫做scores
。 -
*args
本质上为在函数传参时将剩余所有的参数一起打包,所以上面的案例中,我们无论传几个参数,都会被*args
打包,而函数定义中的name
参数则永远都不会有值,而name
不是可变参数,属于必传参数,所以会直接报错 -
由于必传参数可以通过指定参数名称传入,所以当定义函数时必传参数位于可变参数后,调用时就必须指定参数名称
大家可以假设一下如果不用*args
的话会怎么样?以Python中最常用的print
函数为例,定义如下
def print(self, *args, sep=' ', end='\n', file=None): # known special case of print
pass
print(1, 2, 3) # 输出 1 2 3
根据print
函数的定义,可以直接print(1, 2, 3)
,或者传入任何数量个想要打印输出的变量,那么不用*args
的话,改写如下:
def print(self, args, sep=' ', end='\n', file=None): # known special case of print
pass
print([1, 2, 3]) # 输出 1 2 3
同样要实现打印任何数量个变量,我们就必须将其组合成一个数组或元组传入,上述打印[1, 2, 3]
的例子还勉强可以接受的话,那么如果想要打印很多不同类型的变量的时候,还需要将这些不同类型的变量先组成一个数组,这就会让代码显得很怪异,似乎不符合逻辑,而且代码的可读性也会下降。
三、**kwargs
*
操作其实本质上讲就是对于可迭代对象的解包,对于列表和元组这种可迭代对象,*
就可以直接将其解包成一个一个的元素,但字典同样是可迭代对象,那么对字典使用*
操作会是什么样的结果
def student(name, age):
print("My name is ", name)
print("I am %s years old." % age)
student(*{"name": "ethan", "age": 18}) # 输出 My name is name \n I am age years old.
student(*{"Name": "ethan", "Age": 18}) # 输出 My name is Name \n Iam Age years old.
从上面的输出结果可以看出,*
解包操作似乎只是讲字典的键做了解包,所以上述的函数调用其实等价于
fun(*["name", "age"])
fun(*["Name", "Age"])
那么如果想对键和值都做解包要怎么实现呢?
在Python中对于字典的打包解包要使用**
,如下:
student(**{"name": "ethan", "age": 18}) # 输出 My name is ethan \n I am 18 years old.
- 对于字典解包进行传参,必须保证字典的键和函数定义的形参名称一致
很多朋友对于
*
和**
总是搞混淆,其实我的记法是,*
可以相当于对可迭代对象的元素寻址,但根据字典的底层数据结构,字典底层是一个二维数组,那么对于二维数组的元素寻址就需要使用两个*
。
同样的如果我们定义函数时无法确定需要传入什么样的关键字参数呢?那么就可以在定义函数时使用关键字参数的打包语法
def student(name, age, *scores, **kwargs):
print("My name is ", name)
print("I am %s years old." % age)
print("My scores are ", *scores)
print("My avg score is ", sum(scores)/len(scores))
print("Other information:", kwargs)
student("ethan", 18, 80, 90, 98, gender="male", weight=70)
''' 输出
My name is ethan
I am 18 years old.
My scores are 80 90 98
My avg score is 89.33333333333333
Other information: {'gender': 'male', 'weight': 70}
'''
student("ethan", 18, 80, 90, 98, **{"gender":"male", "weight":70}) # 输出同上
-
*kwargs
同样是一个约定俗成的写法,没有其他特殊含义,但是为了代码可读性,最好还是用约定俗成的。 -
*args
是把多个参数打包成元组,而**kwargs
是把多个关键字参数打包成字典。
【总结】
一般特殊技巧虽然好用,但都有一些额外需要注意的地方,所以显而易见的是我们这篇文章太“干”了,但这又是想写出更Pythonic的代码所必须要经历的。
最后留一个小问题供大家思考,为什么不能使用print(**kwargs)
来打印关键字参数解包后的结果?
欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容