python中*号用法大全

3 minute read

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

4.代表乘法符号