python中*号用法大全
Published:
python中*号用法大全。
1.通过带星号的unpacking操作来捕获多个元素,不要用切片
Python新手经常通过下标与切片来从一个列表中取出想要的量,例如
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
oldest = car_ages_descending[0]
second_oldest = car_ages_descending[1]
others = car_ages_descending[2:]
print(oldest, second_oldest, others)
这样做没问题,但是下标与切片会让代码看起来很乱。而且,用这种办法把序列中的元素分成多个子集合,其实很容易出错,因为我们通常容易把下标多写或少写一个位置。例如,若修改了其中一行,但却忘了更新另一行,那就会遇到这种错误。
这个问题通过带星号的表达式(starred expression)来解决会更好一些,这也是一种unpacking操作,它可以把无法由普通变量接收的那些元素全都囊括进去。下面用带星号的unpacking操作改写刚才那段代码,这次既不用取下标,也不用做切片。
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
oldest, second_oldest, *others = car_ages_descending
print(oldest, second_oldest, others)
这样写简短易读,而且不容易出错,因为它不要求我们在修改完其中一个下标之后,还必须记得同步更新其他的下标。 这种带星号的表达式可以出现在任意位置,所以它能够捕获序列中的任何一段元素。
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
oldest, *others, youngest = car_ages_descending
print(oldest, youngest, others)
*others, second_youngest, youngest = car_ages_descending
print(youngest, second_youngest, others)
unpacking操作可以用在可迭代对象上,即在元组、列表、集合和字典内部进行对可迭代对象直接解包。其他情况是不允许对可迭代对象直接解包。
it = (*range(1,4),) #在元组内
print(it)
it = [*range(1,4)] #在列表内
print(it)
it = {*range(1,4)} #在集合内
print(it)
it = *range(1,4), #在元组内
print(it)
但是不允许直接解包,例如
it = *range(1,4)
print(it)
这样直接解包就是错误的。
unpacking操作也可以用在迭代器上(注意迭代器与可迭代对象的区别,可迭代对象通过iter函数便可以变成迭代器对象)。 首先看一个直观的例子
it = iter(range(1, 5))
first, *others = it
print(f'{first} and {others}')
但是这样写与把数据拆分到多个变量里面的那种基本写法相比,并没有太大优势。
对迭代器做unpacking操作的好处,主要体现在带星号的用法上面,它使迭代器的拆分值更清晰。例如,这里有个生成器,每次可以从含有整个一周的汽车订单的CSV文件中取出一行数据。如果用下标和切片来处理这个生成器所给出的结果,但这样写需要很多行代码,而且看着比较混乱。
def generate_csv():
yield ('Date', 'Make' , 'Model', 'Year', 'Price')
for i in range(100):
yield ('2019-03-25', 'Honda', 'Fit' , '2010', '$3400')
yield ('2019-03-26', 'Ford', 'F150' , '2008', '$2400')
all_csv_rows = list(generate_csv())
header = all_csv_rows[0]
rows = all_csv_rows[1:]
print('CSV Header:', header)
print('Row count: ', len(rows))
利用带星号的unpacking操作,我们可以把第一行(表头)单独放在header变量里,同时把迭代器所给出的其余内容合起来表示成rows变量。这样写就清楚多了。
def generate_csv():
yield ('Date', 'Make' , 'Model', 'Year', 'Price')
for i in range(100):
yield ('2019-03-25', 'Honda', 'Fit' , '2010', '$3400')
yield ('2019-03-26', 'Ford', 'F150' , '2008', '$2400')
it = generate_csv()
header, *rows = it
print('CSV Header:', header)
print('Row count: ', len(rows))
2.作为数量可变的位置参数,让函数的参数列表更加清晰
这个就是常见的在函数的形参当中有*args
的情况。下面来看一下*args
的好处。
假设我们要记录调试信息。如果采用参数数量固定的方案来设计,那么函数应该接受一个表示信息的message参数和一个values列表(这个列表用于存放需要填充到信息里的那些值)。
def log(message, values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print(f'{message}: {values_str}')
log('My numbers are', [1, 2])
log('Hi there', [])
即便没有值需要填充到信息里面,也必须专门传一个空白的列表进去,这样显得多余,而且让代码看起来比较乱。最好是能允许调用者把第二个参数留空。在Python里,可以给最后一个位置参数加前缀*,这样调用者就只需要提供不带星号的那些参数,然后可以不再指其他参数,也可以继续指定任意数量的位置参数。函数的主体代码不用改,只修改调用代码即可。
def log(message, *values): # The only difference
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print(f'{message}: {values_str}')
log('My numbers are', 1, 2)
log('Hi there') # Much better
如果想把已有列表里面的元素当成参数传递给上面定义的log这样的参数个数可变的函数,在传参时可以使用*
操作符,python会用unpacking机制,把列表里面的元素取出传给函数。
def log(message, *values): # The only difference
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print(f'{message}: {values_str}')
favorites = [7, 33, 99]
log('Favorite colors', *favorites)
log('Favorite colors', favorites)
使用*args
虽然方便,但是要注意一个容易出现bug的写法。如果要给log函数在形参的第一个位置添加一个参数,那么之前的调用将会出错。
def log(sequence, message, *values):
if not values:
print(f'{sequence} - {message}')
else:
values_str = ', '.join(str(x) for x in values)
print(f'{sequence} - {message}: {values_str}')
log(1, 'Favorites', 7, 33) # New with *args OK
log(1, 'Hi there') # New message only OK
log('Favorite numbers', 7, 33) # Old usage breaks
第一次和第二次调用都是正确的,但是注意看第三次调用。第三次调用中,’Favorite numbers’传给了sequence参数,7 传给了message参数,33 传给了*values参数。但是这并不是想要的正确的调用结果。但是这种bug并不会报错,因此,在使用*args
时,应该限制函数的参数传递只能通过关键字来指定。也就是下面一条。
3.限定函数的形参只能通过关键字指定
看下面一种常见的情况,在编写很多函数时,我们会添加一些代表开关的量来控制不同的行为。
def safe_division_b(number, divisor,
ignore_overflow=False, # Changed
ignore_zero_division=False): # Changed
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
上面的程序我们想通过ignore_overflow和ignore_zero_division来控制除数为0和数值溢出情况的处理办法。如果调用者写出了这样的代码:
assert safe_division_b(1.0, 10**500, True, False) == 0
这种写法一是并不能清晰的看出是控制什么为True,二是容易出现不易发现的bug。
这种情况可以使用在形参中添加*
来限制*
之后的形参必须按照关键字指定的方式传入。例如
def safe_division_c(number, divisor, *, # Changed
ignore_overflow=False,
ignore_zero_division=False):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
这时候,再按照上面的调用方法就会出现错误。
try:
safe_division_c(1.0, 10**500, True, False)
except:
logging.exception('Expected')
else:
assert False
而必须使用关键字指定的方式传入。
assert safe_division_c(number=2, divisor=5) == 0.4
assert safe_division_c(divisor=5, number=2) == 0.4
assert safe_division_c(2, divisor=5) == 0.4
通过这种限制,就只能以关键字指定形参名称的方式传入。
result = safe_division_c(1.0, 0, ignore_zero_division=True)
assert result == float('inf')
result = safe_division_c(1.0, 10**500, ignore_zero_division=False,ignore_overflow=True)
assert result == 0
result = safe_division_c(1.0, 10**500, ignore_zero_division=True,ignore_overflow=False)
assert result == 0