引言
关于Python系列的文章,已经通过两篇文章,介绍了Python中关于函数的简单使用,包括为什么要使用函数,以及函数中带默认值参数的使用注意事项。
之后,岔开函数的主题,通过几篇番外篇,重点谈了一下Python中一切皆对象、Python的数据模型,以及Python中函数和类也是一等公民的理念。其实,关于函数也一直有所涉及。
今天,准备结合Python中星号(star)操作符的用法,继续展开Python中函数的介绍。
关于星号(*)的使用,主要内容有:
1、基础的乘法运算
2、字符串的重复
3、列表的扩充
4、定义不定长的函数位置参数
5、函数调用时,将列表拆包为位置参数进行传递
6、定义不定长的函数关键字参数
7、函数调用时,将字典拆包为关键字参数进行传递
python中的乘法运算
*号是所有编程语言中,比较常见的操作符,首先能够想到的就是四则运算中的乘法运算。没错,首次接触编程语言的新手,可能在键盘上找不到表示乘法运算的x。在所有编程语言中,都是使用*的。
# *的基础作用,乘法运算:
a = 10
b = 5
print(f"{a}乘以{b}的结果是:{a * b}")
# Python中也支持复合赋值的操作:
# 等价于 b = b * 100
b *= 100
print(b)
执行结果:
除了数字可以进行乘法运算,Python中将乘法运算进行了扩充:
字符串可以与数字进行相乘,进行字符串的重复
# 书读百遍,其义自现;百遍太多,十遍也行
s1 = 'Life is short, I use Pyton\n'
s2 = s1 * 10
print(s2)
执行结果:
列表也可以进行乘法运算的扩充:
# 一个不过瘾,我要打十个
enemies = ['小喽啰']
print(id(enemies))
enemies *= 10
print(id(enemies))
print(enemies)
执行结果:
这里的复合赋值操作,实际上进行了原列表对象的扩充,而非重新构造一个列表对象。
自动打包
在前面的文章中,介绍Python的unpacking机制时,已经用到过*号,将不需要的元素进行装包,这里简单回顾一下:
# * unpacking
persons = [('张三', 18, 190, '女'), ('小红', 23, 165, '男')]
for p in persons:
name, *others, gender = p
print(others)
print(type(others))
print(f"姓名: {name}, 性别: {gender}")
还是,之前的代码,我们通过*号修饰others变量,从而将除了name、gender接收的元素外,统一打包为一个列表,交由others变量来进行接收:
函数中的位置参数
相较于其他编程语言中的函数调用,形参列表和实参列表必须一一对应,按顺序传递(默认值参数除外)。Python中将必须按照顺序一一传递的函数调用传参,称为位置参数:
a = 10
b = 5
result = divide(a, b)
print(f"{a}除以{b}的结果是:{result}")
我们很容易碰到的一个问题是,如何定义一个函数,可以接收不定数目的参数,比如以下实际场景:
我们如何定义一个函数,可以计算若干个数字的和?
在其他编程语言中,可以通过函数重载来实现,可以定义多个同名的函数,但是参数列表的参数类型、个数不同。
但是,Python中似乎不支持函数重载。
很容易想到的一个变通的解法是我们定义一个接收一个列表作为参数的函数,列表中有几个元素,就是对几个元素求和:
# 定义一个接收列表的函数,从而变通地实现不定数目的参数求和
def my_sum(args):
result = 0
for num in args:
result += num
return result
nums = [1, 2, 3, 4, 5]
print(my_sum(nums))
这样,确实能够实现不定长参数的问题,但是,每次调用函数,都必须包装成一个列表,然后才能进行传参,似乎有点不太方便。
不定长位置参数
其实,Python中有更好的解决方案:通过*号,让函数接收数量可变的位置参数,可以让函数的设计、使用更加清晰。
这些位置参数通常简称为varargs,或者叫作star args,因为在Python中,我们习惯于使用*args来进行数量可变的位置参数的定义(注意,只是习惯,可以是别的命名方式)。
接下来,还是关于数字求和的问题,我们看star args的函数定义方式:
# 定义star args的函数,实现不定数目的参数求和
def my_sum(*args):
result = 0
for num in args:
result += num
return result
# 函数调用
print(my_sum(1, 2, 3, 4, 5))
可以发现,只需要将原来的函数定义的地方,参数前面加个*就可以解决这个问题了。
函数的调用方式,变成了直接传具体的数字就行了,有多少传多少,不需要包装成列表对象了。
如果由于历史原因,已经将参数包装成列表了,怎么办呢,也可以通过调用时加*进行unpacking处理:
# 定义star args的函数,实现不定数目的参数求和
def my_sum(*args):
result = 0
for num in args:
result += num
return result
# 列表调用
nums = [1, 2, 3, 4, 5]
print(my_sum(*nums))
不定长的star args,也可以跟固定的位置参数混合使用:
# 定义star args的函数,实现不定数目的参数求和
def my_sum(initial_value, *args):
result = initial_value
for num in args:
result += num
return result
# 列表调用
nums = [1, 2, 3, 4, 5]
print(my_sum(100, *nums))
有些教材中,描述说固定的位置参数必须放在star args之前进行定义,这种说法其实是不严谨的。
这两种参数,可以理解为一个是必选的参数,另一个是可选的参数。由于都是按照位置来传,就没法区分,哪个是由必选参数接收,哪个又是由可选参数接收了。
Python中,通过关键字参数机制来避免位置参数传参的二义性。
函数中的关键字参数
由于通过位置参数进行传参,必须顺序一一对应,稍不留神,可能就会导致错误,而且后续函数扩展,增加新的位置参数,所有调用的地方都要按照顺序调整……
好在Python中除了位置参数的函数调用方式,还可以通过关键字进行传参,可以避免位置错乱的问题,回到前面的除法的函数:
# 简单的关键字参数调用演示
def divide(a, b):
return a / b
# 我们都一样
print(divide(10, 5))
print(divide(10, b=5))
print(divide(a=10, b=5))
print(divide(b=5, a=10))
回到上面关于数量不定的位置参数,并不是star args后面,就不能定义必选参数了,只是调用时需要使用关键字参数进行调用而已。
所以,Python中关于函数参数定义的规定是:在star args之后定义的参数,都是关键字参数,调用的时候,只能使用关键字参数的方式进行参数传递:
# 定义star args的函数,实现不定数目的参数求和
def my_sum(*args, initial_value):
result = initial_value
for num in args:
result += num
return result
# 列表调用
nums = [1, 2, 3, 4, 5]
# 这种定义方式会报错,可以清晰地看到不定长位置参数与关键字参数混用的限制
print(my_sum(*nums, 100))
运行报错:
必须以关键字参数的形式传递initial_value这个参数。正确的调用方式:
print(my_sum(*nums, initial_value=100))
print(my_sum(initial_value=100, *nums))
从Python 3.8开始,哪怕函数不需要不定长的位置参数,也可以在函数定义中,使用*号,来强制要求之后的参数必须作为关键字参数进行传参:
# Python3.8开始的,*使用
def star_func(a, b, *, c, d):
print(f"a: {a}, b: {b}, c: {c}, d:{d}")
# 报错 TypeError: star_func() takes 2 positional arguments but 4 were given
# star_func(1, 2, 3, 4)
# 报错 TypeError: star_func() missing 2 required keyword-only arguments: 'c' and 'd'
# star_func(1, 2)
# 正确调用方式:
star_func(1, 2, d=10, c=5)
不定长关键字参数
位置参数可以不定数目,实现函数的一次定义,灵活调用。关键字参数,其实也是可以的。这就是要介绍的两颗星的使用了(**)
在Python函数定义中的两个习惯:
- *args:进行不定长的位置参数的定义
- **kwargs:进行不定长的关键字参数的定义
再次说明,只是习惯,args、kwargs的参数名是可以随意的。
以一个简单的示例说明下不定长关键字参数的定义:
def double_star_func(**kwargs):
print("传递的关键字参数有:")
for key, value in kwargs.items():
print(f"key: {key}, value: {value}")
double_star_func(a=1, b=2, c='hello python')
double_star_func(host='127.0.0.1', port=3306, user='deploy', database='test')
此外,如同我们可以通过列表来进行位置参数的传递,关键字参数,我们可以通过字典对象进行传递。
通过将我们已有的存储在字典中的配置信息,以关键字参数的方式进行传递,可以极大地简化函数的调用。
比如,有如下场景,我们需要进行MySQL的数据库连接,我们以字典对象的方式存储了多个数据库的连接配置信息:
第一种连接数据库的函数调用方式:
import pymysql
db_configs = {
'dev': {
'host': '127.0.0.01',
'port': 3306,
'user': 'dev_user',
'password': 'dev_password',
'database': 'db_dev'
},
'test': {
'host': '127.0.0.01',
'port': 3306,
'user': 'test_user',
'password': 'test_password',
'database': 'db_test'
},
'prod': {
'host': '127.0.0.01',
'port': 3306,
'user': 'prod_user',
'password': 'prod_password',
'database': 'db_prod'
}
}
dev_db = db_configs['dev']
db_conn = pymysql.connect(host=dev_db['host'], port=dev_db['port'], user=dev_db['user'], password=dev_db['password'], database=dev_db['database'])
有点麻烦……
第二种连接方式:我们看是用字典传参的方式:
import pymysql
db_configs = {
'dev': {
'host': '127.0.0.01',
'port': 3306,
'user': 'dev_user',
'password': 'dev_password',
'database': 'db_dev'
},
'test': {
'host': '127.0.0.01',
'port': 3306,
'user': 'test_user',
'password': 'test_password',
'database': 'db_test'
},
'prod': {
'host': '127.0.0.01',
'port': 3306,
'user': 'prod_user',
'password': 'prod_password',
'database': 'db_prod'
}
}
db_conn = pymysql.connect(**db_configs['dev'])
在函数调用时,可以通过两个星号(**)来将字典解析为函数的关键字参数进行传递,从而简化函数的调用。
总结
在Python中,星号(*)的主要用法有:
1、基础的乘法运算
2、字符串的重复
3、列表的扩充
4、定义不定长的函数位置参数
5、函数调用时,将列表拆包为位置参数进行传递
6、定义不定长的函数关键字参数
7、函数调用时,将字典拆包为关键字参数进行传递
只要理解了这几点,在后续的使用中,就能大大简化自己代码的编写,真正提高代码的编写效率,毕竟我们选择用Python的原因在于人生苦短。