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

news2024/9/25 19:17:08

目录

引言

日志打印的问题

返回参数默认值的问题

问题产生的原因

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

总结


引言

在前一篇关于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:用户付款成功')

执行结果:

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

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1920410.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

动态规划之数字三角形模型+最长上升子序列模型

首先,我们从集合角度重新看待DP: 直接看题:https://www.acwing.com/problem/content/1029/ 就是取纸条的原题,我们令f[i1,j1,i2,j2]表示从(1,1),(1,1)分别走到(i1,j1),(i2,j2)的路径的max i1j1i2j2,于是我们可以把状…

ESP32的芯片有几种

ESP32系列 ESP32芯片截止到24年7月有5个系列: ESP32-C3 ESP32由ESP32-P 系列、ESP32-S 系列、ESP32-C 系列、ESP32-H 系列、ESP32 系列构成。其中ESP32-S分为S3和S2两个小系列;ESP32-C系列分为C6、C5、C3、C2四个小系列,具体如下。 说明&am…

合宙 Air780E模块 AT 指令 MQTT连接

固件说明 重启模块 //tx ATRESET//rx ATRESETOK ^boot.romv!\n RDY^MODE: 17,17E_UTRAN ServiceCGEV: ME PDN ACT 1NITZ: 2024/07/10,08:33:440,0查询模块版本信息 //tx ATCGMR//rx ATCGMRCGMR: "AirM2M_780E_V1161_LTE_AT"OK基本流程 4G模块支持MQTT和MQTT SSl协…

5 MySql

5 MySql 一、简介二、SQL语言2.1 导入外部SQL文件2.2 显示表结构2.3 与创建数据库相关的语句2.4 与表相关的语句2.5 操作表中的数据2.6 7种基本的sql查询 三、SQL的注意点3.1 与集合函数相关3.2 SQL语句的书写与执行过程 四、约束 constraint4.1 作用4.2 功能分类4.3 自增 五、…

读人工智能全传10深度思维

1. 深度思维 1.1. DeepMind 1.1.1. 深度思维 1.1.2. 2014年的员工不足25人 1.1.3. 深度思维公司公开宣称其任务是解决智能问题 1.1.4. 2014年谷歌收购DeepMind,人工智能突然成了新闻热点,以及商业热点 1.1.4.1. 收购报价高达4亿英镑 1.1.4.2. 深度…

差分约束——AcWing 362. 区间

差分约束 定义 差分约束系统是一种在计算机科学和运筹学中用于解决特定类型优化问题的工具。它主要用于处理一类线性不等式组,这些不等式描述了变量之间的相对大小关系,而不是直接的绝对值大小。差分约束系统通常用于路径寻找、调度、资源分配等问题。…

maven私有镜像仓库nexus部署使用

maven私有镜像仓库nexus部署使用 1、Nexus部署 #查找镜像 docker search sonatype/nexus3 #拉取镜像 docker pull sonatype/nexus3 #持久化目录 mkdir -p /data/nexus/data chmod 777 -R /data/nexus/data #启动服务 docker run -d --name nexus3 -p 8081:8081 --restart alw…

javaweb基础知识入门

javaweb 1.基本概念 1.1前言 web开发: web,网页的意思,www.baidu.com 静态web html,css 提供给所有人看的数据始终不会发生变化! 动态web 淘宝...等几乎是所有的网站 提供给所有人看的数据始终会发生变化&#…

mac生成.dmg压缩镜像文件

mac生成.dmg压缩镜像文件 背景准备内容步骤1,找一个文件夹2,制作application替身1,终端方式2,黄金右手方式 3,.app文件放入文件夹4,制作.dmg压缩镜像文件5,安装.dmg 总结 背景 为绕开App Store…

头歌资源库(27)特别的数

一、 问题描述 编程输出一个特别的数,该数是一个由1~9组成的9位数,每个数字只能出现一次,且这个9位数由高位到低位前i位能被i整除。 二、算法思想 创建一个长度为9的数组,用于存放1~9这9个数字。使用回溯算法,从第…

Visual Studio 2019 (VS2019) 中使用 CMake 配置 OpenCV 库(快捷版)

2024.07.11 测试有效 最近需要用一下 opencv 处理图像,简单配置了一下Cmake下的 opencv 库。 没有编译 opencv ,也不知道他们为什么要自己编译 opencv 。 一、下载并安装 OpenCV 1.前往 OpenCV 官方网站 下载适用于您的系统的 OpenCV 安装包。 2.点击直接…

在分布式环境中,怎样保证 PostgreSQL 数据的一致性和完整性?

文章目录 在分布式环境中保证 PostgreSQL 数据的一致性和完整性一、数据一致性和完整性的重要性二、分布式环境对数据一致性和完整性的挑战(一)网络延迟和故障(二)并发操作(三)数据分区和复制 三、保证 Pos…

PFH点特征直方图

PFH特征描述子原理 该算法通过参数化查询关键点与其周围邻域点之间的空间差异,形成一个多维度直方图,从而实现对该点的邻域几何属性的描述。 该方法具有以下三个优势: (1)刚性变换不变性,即不受旋转、平移变换的影响; (2)采样一致性,即改变采样密度,特征保…

【高中数学/指数函数、对数函数、正弦函数】求 y=2^x+x,y=log2_x+x,y=2*sinX+x 的零点位置大小关系

【问题】 已知函数f(x)2^xx,g(x)log2_xx,h(x)2*sinXx 的零点分别是a,b,c,则a、b、c的大小顺序是? 【解答】 粗览三个函数,h(x)2*sinXx的零点是最好解决的,明显x0时h(x)0,因此c在原点的位置; 对于f(x)2^xx&#xff…

css预编译器--sass

Sass Sass 提供了 变量(variables)、嵌套规则(nested rules)、 混合(mixins)、 函数(functions),目前我使用最多的还是变量和嵌套规则,貌似目前css也支持嵌套…

kafka发送消息流程

配置props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, RoundRobinPartitioner.class); public Map<String,Object> producerConfigs(){Map<String,Object> props new HashMap<>();props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,bootstrapServers…

代码随想录算法训练营Day36||动态规划part04

494.目标和&#xff1a;本题的方法主要用来解决------装满容量为x的背包&#xff0c;有几种方法。 可以先理解二维数组的思路&#xff1a;感觉b站一个评论写得很清晰&#xff0c;借用一下。 这题最难理解的地方在于如何初始化数组&#xff0c;为什么dp[0]1&#xff1b;我试图自…

【C++BFS】690. 员工的重要性

本文涉及知识点 CBFS算法 LeetCode690. 员工的重要性 你有一个保存员工信息的数据结构&#xff0c;它包含了员工唯一的 id &#xff0c;重要度和直系下属的 id 。 给定一个员工数组 employees&#xff0c;其中&#xff1a; employees[i].id 是第 i 个员工的 ID。 employees[…

企业数据治理做完了,如何让业务部门用起来

引言&#xff1a;企业数据治理完成后&#xff0c;确保业务部门能够充分利用这些数据并融入日常运营中&#xff0c;是实现数据价值最大化的关键步骤。以下是一些策略和建议&#xff0c;帮助推动业务部门使用数据治理成果&#xff1a; 一、管理层面推广 高层应用示范&#xff1…

kafka与zookeeper的SSL认证教程

作者 乐维社区&#xff08;forum.lwops.cn&#xff09;许远 在构建现代的分布式系统时&#xff0c;确保数据传输的安全性至关重要。Apache Kafka 和 Zookeeper 作为流行的分布式消息队列和协调服务&#xff0c;提供了SSL&#xff08;Secure Sockets Layer&#xff09;认证机制&…