13、Python之函数:简单的参数默认值其实并不简单

目录

引言

日志打印的问题

返回参数默认值的问题

问题产生的原因

关于参数默认值的最佳实践

总结


引言

在前一篇关于Python函数的文章中,我们介绍了函数的基本使用、函数的默认参数、lambda函数的用法,相当于对Python中的函数有了一个入门的介绍。
今天这篇文章打算就上一篇提到的函数的参数默认值,进一步展开来讲。因为,这个看似简单实用的技巧,如果不理解相关的底层细节,可能反而导致意想不到的BUG。

本文先以两个应用参数默认值可能导致的问题来展开,然后探究相关问题产生的底层原理,最后给出应对的最佳实践。

日志打印的问题

假如现在有这样一个日志记录的需求,我们这样简化模拟一下:
1、打印日志只需要记录日志内容,及对应的时间;
2、默认情况下,记录的时间,为当前日志打印的时间即可;但是,不排除业务流程处理时间较长,可能需要记录业务开始时间,而非当前时间的场景,所以要支持传入一个时间的需求。

根据上面的需求,参数默认值,是我们最先想到的,所以,可以定义如下函数:

from datetime import datetime

def log(msg, when=datetime.now()):
    print(f"{when}: {msg}")

但是,实际执行的结果,可能不是我们想要的:

from datetime import datetime
import time

def log(msg, when=datetime.now()):
    print(f"{when}: {msg}")

log('订单001:下单成功')
time.sleep(5)
log('订单001:用户付款成功')

执行结果:


明明等待了5秒,为啥日志打印的时间都是相同的……

返回参数默认值的问题

有些情况下,我们函数需要返回一个容器对象,用户需要基于这个容器进行,进一步的操作,使用了参数默认值可能也是存在问题的。
比如,有如下场景:
api传入的请求参数以字符串的形式拼接在一起,我们需要将其解析为字典格式,并返回,如果这个api没有请求参数,则返回一个空字典。用户需要对返回的请求参数字典进行进一步的处理,比如从cookie中提取信息,比如userid等,加入到请求参数字典中。
根据需求,可能会选择定义一个如下的函数:

def parse_args(request_url, default={}):
    if '?' in request_url:
        args = request_url.split('?')[1]
        return {arg.split('=')[0]: arg.split('=')[1] for arg in args.split('&')}
    return default

正常情况下,应该都是没有问题的,但是,如果走了默认情况下,可能存在问题:

from rich import print
from rich.console import Console

console = Console()

def parse_args(request_url, default={}):
    if '?' in request_url:
        args = request_url.split('?')[1]
        return {arg.split('=')[0]: arg.split('=')[1] for arg in args.split('&')}
    return default

# 用户1的请求
args1 = parse_args('/api/goods/detail?goods_id=123')
args1['userid'] = '1'
# 用户2的请求
args2 = parse_args('/api/goods/list')
args2['userid'] = '2'
# 用户3的请求
args3 = parse_args('/api/store/list')
args3['userid'] = '3'
console.rule('用户1请求参数')
print(args1)
console.rule('用户2请求参数')
print(args2)
console.rule('用户3请求参数')
print(args3)


执行结果:


用户2和用户3都是无参数请求api,可是最终处理完成后,两个请求中的userid都变成了3……

问题产生的原因

不管是日志打印中的默认当前时间,还是请求参数解析的返回空的参数字典,似乎都出现了我们预料之外的情况:函数的多次重复调用,默认值参数的默认值,我们以为在每次发生时,都会变化,我们理解的是无固定值的默认值,可是函数似乎给我们固定住了……

原因在于,参数默认值如果是一个表达式,这个表达式会在函数定义时,计算出来,并生成一个对象,存储下来,以后的每次调用,参数的默认值都指向一个相同的对象。

通过字节码,我们可以更加清晰地看到这一点:
以日志打印为例:

通过如上的字节码与源码的对照,可以轻易发现,函数参数的默认值的计算,确实是在函数定义时完成的,函数调用时,直接取之前计算出来的结果,不会重新计算。

此外,即使不看对应的字节码,我们还有更简单的方法,来看到参数默认值的情况:
由于Python中一切皆对象,函数也是一个特殊的对象,函数对象,有自身的一些属性,其中一个属性就是__defaults__,以元组的形式存储了函数的参数默认值:

from datetime import datetime
import time

def log(msg, when=datetime.now()):
    print(f"{when}: {msg}")

print(log.__defaults__)

time.sleep(5)
log('订单001:下单成功')
time.sleep(5)
log('订单001:用户付款成功')

如上代码,我们在调用函数log()之前,首先输出了log函数对象的__defaults__属性,然后是两次函数调用。

执行结果如下:


两次函数调用,输出的参数默认值,均为函数对象在定义时,存储在函数对象的__defaults__中的默认值。

同样的,在请求参数解析的函数中,我们定义的默认的请求参数空字典对象,也是在定义时生成的。我们可以通过查看函数对象的参数默认值对象的id,以及args2、args3的id,清楚地看到这一点:

from rich import print
from rich.console import Console

console = Console()

def parse_args(request_url, default={}):
    if '?' in request_url:
        args = request_url.split('?')[1]
        return {arg.split('=')[0]: arg.split('=')[1] for arg in args.split('&')}
    return default

# 用户1的请求
args1 = parse_args('/api/goods/detail?goods_id=123')
args1['userid'] = '1'
# 用户2的请求
args2 = parse_args('/api/goods/list')
args2['userid'] = '2'
# 用户3的请求
args3 = parse_args('/api/store/list')
args3['userid'] = '3'
console.rule('用户1请求参数')
print(args1)
console.rule('用户2请求参数')
print(args2)
console.rule('用户3请求参数')
print(args3)

# 新增3行字段,验证参数默认值对应的字典对象,是同一个对象
console.rule('参数默认值对象id')
print(id(parse_args.__defaults__[0]))
print(id(args2))
print(id(args3))

执行结果:

可以看到,3个对象的id是相同的,印证了参数默认值在函数定义时生成对象,并存储到函数对象的__defaults__属性中的论断。

关于参数默认值的最佳实践

关于以上两种场景中,涉及到参数默认值使用中的异常情况,一个相对较好的解决方案是,使用None默认值,并结合docstirng进行使用说明。
同样以日志打印为例,进行代码的改写,以示说明:

from datetime import datetime
import time

def log(msg, when=None):
    """
    根据调用传参,进行日志的打印
    :param msg: 日志内容
    :param when: 日志记录时间,默认为None,表示记录当前时间
    :return:
    """
    if when is None:
        when = datetime.now()
    print(f"{when}: {msg}")

print(log.__defaults__)

time.sleep(5)
log('订单001:下单成功')
time.sleep(5)
log('订单001:用户付款成功')

执行结果:

这次执行,获得了我们想要的结果。

总结

虽然函数参数的默认值,语法很简单,使用很方便。但是,稍微一不留意,可能也会导致一些异常的结果。
基础很简单,但也很重要。
真正掌握基础并不简单,只是把语法记住了,并不是真正掌握。
遇到问题不要慌,关注底层的细节,能够更加容易的定位问题所在,并理解问题的产生。
而所谓的编程学习,学的并不是写几行代码,而是通过写代码,逐渐习得并强化自己定位问题、解决问题的能力。

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值