编写高质量Python (第25条) 用只能以关键字指定和只能按位置传入的参数来设计清晰的参数列表

第25条 用只能以关键字指定和只能按位置传入的参数来设计清晰的参数列表

​ 按关键字传递参数是 Python 函数的一项强大特性(参见 第23条)。这种关键字参数特别灵活,在很多情况下,都能让我们写出一看就能懂的函数代码。

​ 例如,计算两个数字相除的结果时,可能需要仔细考虑各种特殊情况。例如,在除数为 0 的情况下,是抛出 ZeroDivisionError 异常,还是返回无穷(infinity);在结果溢出的情况下,是抛出 OverflowError 异常,还是返回 0 。

def safe_division(number, divisor,
                  ignore_overflow,
                  ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
        

​ 这个函数用起来很直观。如果想在结果溢出的情况下,让它返回 0,那么可以像下面一样调用函数。

result = safe_division(1.0, 10 ** 500, True, False)
print(result)

>>>
0

​ 如果想在除数是 0 的情况下,让函数返回无穷,那么就按下面这样来写。

result = safe_division(1.0, 0, False, True)
print(result)

>>>
inf

​ 表示要不要忽略异常的那两个参数都是 Boolean 值,所以容易弄错位置,这会让程序出现难以寻找的 bug。要想让代码看起来更加清晰,一种方法就是给这两个参数都指定默认值。按照默认值,该函数只要遇到特殊情况,就会抛出异常。

def safe_division_b(number, divisor,
                  ignore_overflow=False,       # changed
                  ignore_zero_division=False): # changed
  ...

​ 调用者可以用关键字指定覆盖其中某个参数的默认值,以调整函数在遇到那种特殊情况的处理方式,同时让另一个参数依然取那个参数自己的默认值。

result = safe_division_b(1.0, 10 ** 500, ignore_overflow=True)
print(result)

result = safe_division_b(1.0, 0, ignore_zero_division=True)
print(result)

>>>
0
inf

​ 然而,由于这些关键参数是可选的,我们没办法要求调用者必须按照关键字形式来指定这两个参数。他们还是可以用传统的写法,按位置给新定义的 safe_division_b 函数传递参数。

assert safe_division_b(1.0, 10**500, True, False) == 0

​ 对于这种参数比较复杂的函数,我们可以声明只能通过关键字指定的参数(keyword-only argument),这样的话,写出来的代码就能清楚反映调用者的想法了。这种参数只能用关键字指定,不能按位置传递。

​ 下面就重新定义 safe_division 函数,让它接收这样的参数。参数列表里的 * 符号把参数分成两组,左边是位置参数,右边是只能用关键字指定的参数。

def safe_division_c(number, divisor,*,  # changed
                  ignore_overflow=False,       
                  ignore_zero_division=False): 
  ...

​ 如果按位置给只能用关键字指定的参数传值,那么程序就会出错。

result = safe_division_c(1.0, 10 ** 500, True, False)

>>>
Traceback ...
TypeError: safe_division_c() takes 2 positional arguments but 4 were given

​ 当然我们还是可以像前面那样,用关键字参数指定覆盖其中一个参数的默认值(即忽略其中一种特殊情况,并让函数在遇到另一种特殊情况时抛出异常)。

result = safe_division(1.0, 0, ignore_zero_division=True)
assert result == float('inf')
try:
    result = safe_division_c(1.0, 0)
except ZeroDivisionError:
    pass  # Expected 

​ 这样改依然有问题,因为在 safe_division_c 版本的函数里面,有两个参数(也就是 number 和 division)必须由调用者提供。然而,调用者在提供这两个参数时,既可以按位置提供,也可以按关键字提供,还可以把这两种方式混起来用。

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

​ 在未来也许会因为扩展函数的需要,甚至是因为代码风格的变化,或许要修改这两个参数的名字。

def safe_division_c(numerator, denominator,*,  # changed
                  ignore_overflow=False,       
                  ignore_zero_division=False): 
  ...

​ 这看起来只是文字上面的微调,但之前通过关键字形式来指定这两个参数的调用代码都会出错。

safe_division_c(number=2, divisor=5) == 0.4

>>>
Traceback ...
TypeError: safe_division_c() got an unexpected keyword argument 'number'

​ 其实最重要的问题在于,我们根本没打算把 number 和 divisor 这两个名称纳入函数的接口;我们只是在编写函数的实现代码时,随意挑选了这两个比较顺口的名称而已。

​ **Python 3.8 引入了一项新的特性,可以解决这个问题,就是只能按位置传递的参数 (positional-only argument)。**这种参数与刚才的只能通过关键字指定的参数(keyword-only argument) 相反,它们必须按照位置指定,绝不通过关键字形式指定。

​ 下面我们来重新定义 safe_division 函数,使其前两个必须由调用者提供的参数位置来提供。参数列表中的 / 符号,表示它左边的那些参数只能按照位置指定。

def safe_division_d(numerator, denominator, /, *,  # changed
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        return numerator / denominator
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

​ 下面来验证一下,看看调用者按照位置提供了两个参数后,能否得到正确结果。

assert safe_division_d(2, 5) == 0.4

​ 假如调用者是通过关键字形式指定这两个参数的,那么程序就会在运行时抛出异常。

safe_division_d(numerator=2, denominator=5)

>>>
Traceback ...
TypeError: safe_division_d() got some positional-only arguments passed as keyword arguments: 'numerator, denominator'

现在我们可以确信:给 safe_division_d 函数的前两个参数(也就是那两个必备的参数)所挑选的名称,已经与调用者的代码解耦了。即便以后再次修改这两个参数的名称,也不会影响已经写好的调用代码。

​ 在函数的参数列表中,/ 符号左侧的参数是智能按位置指定的参数,* 符号右侧的参数则是只能按关键字形式指定的参数。那么,这两个符号如果同时出现在参数列表中,又有什么效果呢?这是个值得注意的问题。这意味着,这两个符号的参数,既可以按位置提供,又可以用关键字形式指定(其实,如果不特别说明 Python 函数的参数全都属于这种参数).在设计 API 时,为了体现某编程风格或者实现某些需求,可能会允许某些参数既可以按照位置传递,也可以用关键字形式指定,这样可以让代码易读。例如,给下面的 safe_division 函数的参数列表添加一个可选的 ndigits 参数,允许调用者指定这次除法应该精确到小数点后第几位。

def safe_division_e(numerator, denominator, /,
                    ndigits=10, *,  # changed
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        fraction = numerator / denominator
        return round(fraction, ndigits)  # Changed
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

​ 下面我们用三种方式来调用这个 safe_division_e 函数。ndigits 是个带默认值的普通参数,因此,它既可以按位置传递,也可以用关键字指定,还可以直接省略。

result = safe_division_e(22, 7)
print(result)

result = safe_division_e(22, 7, 5)
print(result)

result = safe_division_e(22, 7, ndigits=2)
print(result)


>>>
3.1428571429
3.14286
3.14
  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值