当前位置: 首页 > news >正文

深入理解Python函数参数传递:可变与不可变对象的实战解析20240914

深入理解Python函数参数传递:可变与不可变对象的实战解析

在Python编程中,函数参数的传递方式对代码的行为有着深远的影响。理解可变和不可变对象如何作为参数传递,不仅有助于避免常见的陷阱,还能编写出更高效、可维护的代码。本文将深入探讨Python函数参数传递机制,结合实战例子,揭示其中的奥秘。

一、Python的参数传递机制

Python采用的是**“共享传参”(Call by Sharing)的机制,也称为“对象引用传递”**。这意味着函数接收到的是参数中对象的引用的副本,而不是对象本身。当对象是可变类型时,函数内部对对象的修改可能会影响到外部;当对象是不可变类型时,函数内部的修改不会影响外部。

二、可变对象作为参数

可变对象如listdictset等,在函数中作为参数时,函数内部对其修改会影响到外部对象。

def modify_list(lst):print("函数开始执行")lst.append(0)print(f"函数内列表: {lst}, ID: {id(lst)}")print("函数结束执行")my_list = []
print(f"初始列表ID: {id(my_list)}")
modify_list(my_list)
print(f"函数外列表: {my_list}, ID: {id(my_list)}")

输出:

初始列表ID: 140583281394304
函数开始执行
函数内列表: [0], ID: 140583281394304
函数结束执行
函数外列表: [0], ID: 140583281394304

解析:

  • my_list在函数内外的id相同,说明它们引用的是同一个对象。
  • 函数内对lst的修改直接影响到了函数外的my_list

三、不可变对象作为参数

不可变对象如intstrtuple等,在函数中作为参数时,函数内部创建的新对象不会影响外部对象。

def modify_string(s):print("函数开始执行")s += 'a'print(f"函数内字符串: {s}, ID: {id(s)}")print("函数结束执行")my_str = "hello"
print(f"初始字符串ID: {id(my_str)}")
modify_string(my_str)
print(f"函数外字符串: {my_str}, ID: {id(my_str)}")

输出:

初始字符串ID: 140583281997616
函数开始执行
函数内字符串: helloa, ID: 140583281997808
函数结束执行
函数外字符串: hello, ID: 140583281997616

解析:

  • 函数内的s和函数外的my_strid不同,说明s在函数内部被赋值为一个新对象。
  • 函数内的修改不影响函数外的my_str

四、函数中的默认参数与可变对象

1. 默认参数的陷阱

使用可变对象作为函数的默认参数时,需要谨慎,因为默认参数只在函数定义时计算一次。

def append_to_list(value, lst=[]):lst.append(value)print(f"列表内容: {lst}, ID: {id(lst)}")append_to_list(1)
append_to_list(2)

输出:

列表内容: [1], ID: 140583281394368
列表内容: [1, 2], ID: 140583281394368

解析:

  • 默认参数lst在函数定义时被创建,并绑定到函数对象上。
  • 每次调用append_to_list,如果不提供lst参数,都会使用这个默认的列表对象。
  • 因此,lst的内容会在不同的函数调用间累积,导致意想不到的结果。

2. 解决方案:使用None作为默认参数

为避免上述问题,建议将默认参数设置为None,并在函数内部进行初始化。

def append_to_list(value, lst=None):if lst is None:lst = []lst.append(value)print(f"列表内容: {lst}, ID: {id(lst)}")append_to_list(1)
append_to_list(2)

输出:

列表内容: [1], ID: 140583281394432
列表内容: [2], ID: 140583281395008

解析:

  • 每次调用append_to_list时,lst默认是None,在函数内部被初始化为新的空列表。
  • 这样,每次函数调用都有独立的lst,互不影响。

五、更多关于可变与不可变对象的实战

1. 修改参数的引用

有时,在函数内部重新赋值一个新的对象给参数,会导致参数的引用被修改,而不会影响外部对象。

def reset_list(lst):print("函数开始执行")lst = []print(f"函数内列表: {lst}, ID: {id(lst)}")print("函数结束执行")my_list = [1, 2, 3]
print(f"初始列表: {my_list}, ID: {id(my_list)}")
reset_list(my_list)
print(f"函数外列表: {my_list}, ID: {id(my_list)}")

输出:

初始列表: [1, 2, 3], ID: 140583281395200
函数开始执行
函数内列表: [], ID: 140583281395328
函数结束执行
函数外列表: [1, 2, 3], ID: 140583281395200

解析:

  • 函数内对lst重新赋值为[],修改了lst的引用,但不影响函数外的my_list
  • 函数内外的列表id不同,说明它们是不同的对象。

2. 深拷贝与浅拷贝

当需要在函数内部修改对象,但不希望影响外部对象时,可以使用拷贝。

import copydef modify_list_copy(lst):lst_copy = lst.copy()  # 或使用 lst_copy = copy.deepcopy(lst)lst_copy.append(4)print(f"函数内列表: {lst_copy}, ID: {id(lst_copy)}")my_list = [1, 2, 3]
modify_list_copy(my_list)
print(f"函数外列表: {my_list}, ID: {id(my_list)}")

输出:

函数内列表: [1, 2, 3, 4], ID: 140583281395456
函数外列表: [1, 2, 3], ID: 140583281395200

解析:

  • 使用lst.copy()创建了lst的浅拷贝,在函数内部对拷贝进行修改,不影响原列表。

六、深入理解 *args**kwargs

当函数的参数数量不确定时,可以使用*args**kwargs来接收任意数量的位置参数和关键字参数。

1. 使用 *args 接收任意数量的位置参数

def print_args(*args):print(f"参数类型: {type(args)}, 值: {args}")for index, value in enumerate(args):print(f"索引: {index}, 值: {value}")print_args('a', 'b', 'c')

输出:

参数类型: <class 'tuple'>, 值: ('a', 'b', 'c')
索引: 0, 值: a
索引: 1, 值: b
索引: 2, 值: c

解析:

  • *args将传入的多个位置参数打包成一个元组args
  • 可以通过遍历args来访问所有位置参数。

2. 使用 **kwargs 接收任意数量的关键字参数

def print_kwargs(**kwargs):print(f"参数类型: {type(kwargs)}, 值: {kwargs}")for key, value in kwargs.items():print(f"键: {key}, 值: {value}")print_kwargs(a=1, b=2, c=3)

输出:

参数类型: <class 'dict'>, 值: {'a': 1, 'b': 2, 'c': 3}
键: a, 值: 1
键: b, 值: 2
键: c, 值: 3

解析:

  • **kwargs将传入的多个关键字参数打包成一个字典kwargs
  • 可以通过遍历kwargs.items()来访问所有关键字参数。

3. 混合使用 *args**kwargs

def print_all(*args, **kwargs):if args:print(f"位置参数: {args}")if kwargs:print(f"关键字参数: {kwargs}")print("函数执行结束")print_all('hello', 'world', name='Python', version=3.8)

输出:

位置参数: ('hello', 'world')
关键字参数: {'name': 'Python', 'version': 3.8}
函数执行结束

解析:

  • 函数同时接收任意数量的位置参数和关键字参数。
  • *args**kwargs的位置不能颠倒,*args必须在**kwargs之前。

4. 参数的解包

可以使用***对序列和字典进行解包,将它们的元素作为单独的参数传递给函数。

args = [1, 2, 3]
kwargs = {'a': 4, 'b': 5}def func(*args, **kwargs):print(f"args: {args}")print(f"kwargs: {kwargs}")func(*args, **kwargs)

输出:

args: (1, 2, 3)
kwargs: {'a': 4, 'b': 5}

解析:

  • 使用*args解包列表,将其元素作为位置参数传递。
  • 使用**kwargs解包字典,将其键值对作为关键字参数传递。
  • *args**kwargs的位置不能颠倒,*args必须在**kwargs之前。

七、实践中的最佳实践

1. 避免在函数定义中使用可变对象作为默认参数

错误的示例:

def accumulate(value, total=[]):total.append(value)print(f"累计值: {total}")return totalaccumulate(1)
accumulate(2)
accumulate(3)

输出:

累计值: [1]
累计值: [1, 2]
累计值: [1, 2, 3]

问题:

  • 每次调用accumulate时,total列表都在之前的基础上继续累加,导致结果累积。
  • 这可能不是我们期望的行为,尤其当函数应该是无副作用的。

正确的示例:

def accumulate(value, total=None):if total is None:total = []total.append(value)print(f"累计值: {total}")return totalaccumulate(1)
accumulate(2)
accumulate(3)

输出:

累计值: [1]
累计值: [2]
累计值: [3]

解析:

  • 使用None作为默认参数,在函数内部初始化新的列表,避免了可变默认参数导致的意外行为。

2. 在函数内部谨慎地修改可变对象,必要时使用拷贝

不谨慎的示例:

def remove_last_item(items):items.pop()print(f"函数内列表: {items}")my_list = [1, 2, 3, 4]
remove_last_item(my_list)
print(f"函数外列表: {my_list}")

输出:

函数内列表: [1, 2, 3]
函数外列表: [1, 2, 3]

问题:

  • 函数修改了外部的my_list,可能导致意外的副作用。

正确的示例:

def remove_last_item(items):items_copy = items.copy()items_copy.pop()print(f"函数内列表: {items_copy}")return items_copymy_list = [1, 2, 3, 4]
new_list = remove_last_item(my_list)
print(f"函数外原列表: {my_list}")
print(f"函数外新列表: {new_list}")

输出:

函数内列表: [1, 2, 3]
函数外原列表: [1, 2, 3, 4]
函数外新列表: [1, 2, 3]

解析:

  • 使用拷贝,确保函数的修改不影响外部的my_list

3. 充分利用 *args**kwargs 来编写灵活的函数接口

示例:

def calculate_average(*args):total = sum(args)count = len(args)average = total / count if count != 0 else 0print(f"传入的数值: {args}")print(f"平均值: {average}")return averagecalculate_average(10, 20, 30)
calculate_average(5, 15)
calculate_average()

输出:

传入的数值: (10, 20, 30)
平均值: 20.0
传入的数值: (5, 15)
平均值: 10.0
传入的数值: ()
平均值: 0

解析:

  • 使用*args,函数可以接收任意数量的数字,计算平均值。

使用 **kwargs

def create_user(**kwargs):user_info = {'name': kwargs.get('name', '匿名'),'age': kwargs.get('age', 0),'email': kwargs.get('email', ''),}print(f"用户信息: {user_info}")return user_infocreate_user(name='Alice', age=30)
create_user(email='bob@example.com', name='Bob')

输出:

用户信息: {'name': 'Alice', 'age': 30, 'email': ''}
用户信息: {'name': 'Bob', 'age': 0, 'email': 'bob@example.com'}

解析:

  • 使用**kwargs,函数可以灵活地处理不同的关键字参数。

4. 使用文档字符串和清晰的命名来提高代码的可读性

示例:

def calculate_operations(a, b):"""计算两个数的加、减、乘、除运算结果。参数:- a (float): 第一个数- b (float): 第二个数返回:- results (dict): 包含加、减、乘、除结果的字典异常:- ZeroDivisionError: 当第二个数为零且进行除法运算时抛出"""results = {'addition': a + b,'subtraction': a - b,'multiplication': a * b,}if b != 0:results['division'] = a / belse:results['division'] = None  # 或者抛出异常return resultsnum1 = 12
num2 = 4
operation_results = calculate_operations(num1, num2)
print(f"运算结果: {operation_results}")

输出:

运算结果: {'addition': 16, 'subtraction': 8, 'multiplication': 48, 'division': 3.0}

解析:

  • 使用清晰的函数名和参数名,提供了详细的文档字符串,方便他人理解和使用。

查看文档字符串:

help(calculate_operations)

输出:

Help on function calculate_operations in module __main__:calculate_operations(a, b)计算两个数的加、减、乘、除运算结果。参数:- a (float): 第一个数- b (float): 第二个数返回:- results (dict): 包含加、减、乘、除结果的字典异常:- ZeroDivisionError: 当第二个数为零且进行除法运算时抛出

八、总结

理解Python的参数传递机制,尤其是可变和不可变对象的行为,对于编写高质量的代码至关重要。通过合理地使用函数参数,避免常见陷阱,如可变对象的默认参数问题,我们可以提高代码的可读性、鲁棒性和可维护性。此外,熟练掌握*args**kwargs的用法,可以编写更加灵活和通用的函数。

在实际开发中,遵循以下最佳实践:

  • 避免在函数定义中使用可变对象作为默认参数。
  • 在函数内部谨慎地修改可变对象,必要时使用拷贝。
  • 充分利用*args**kwargs来编写灵活的函数接口。
  • 使用文档字符串和清晰的命名来提高代码的可读性。

通过深入理解这些概念,我们可以更好地掌控Python编程,提高代码质量,实现更加优雅和高效的解决方案。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Web安全与网络安全:SQL漏洞注入
  • setup函数子传父普通写法
  • centos8构建nginx1.27.1+BoringSSL+http3+lua+openresty
  • STM32——看门狗通俗解析
  • Django日志
  • WebRTC服务器搭建
  • SpringBoot + Vue + ElementUI 实现 el-table 分页功能详解
  • 【信号】SIGCHLD信号--了解
  • error: subprocess-exited-with-error
  • 【数据库】MySQL聚合统计
  • 【vuetify】v-select 无法正常显示,踩坑记录!
  • Vue3生命周期钩子函数(Vue3生命周期)
  • vue3 一次二次封装element-plus组件引发的思考
  • 解决ubuntu 24.04 ibus出现卡死、高延迟问题
  • 解决uniapp视频video组件进入全屏再退出全屏后,cover-view失效的问题
  • [ JavaScript ] 数据结构与算法 —— 链表
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 《Java编程思想》读书笔记-对象导论
  • DOM的那些事
  • Fabric架构演变之路
  • github从入门到放弃(1)
  • js算法-归并排序(merge_sort)
  • php ci框架整合银盛支付
  • Transformer-XL: Unleashing the Potential of Attention Models
  • 关于 Linux 进程的 UID、EUID、GID 和 EGID
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 手写双向链表LinkedList的几个常用功能
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • 如何正确理解,内页权重高于首页?
  • ​Spring Boot 分片上传文件
  • ​什么是bug?bug的源头在哪里?
  • ​数据结构之初始二叉树(3)
  • ​水经微图Web1.5.0版即将上线
  • ‌JavaScript 数据类型转换
  • ![CDATA[ ]] 是什么东东
  • # 利刃出鞘_Tomcat 核心原理解析(七)
  • #Spring-boot高级
  • (16)UiBot:智能化软件机器人(以头歌抓取课程数据为例)
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (回溯) LeetCode 46. 全排列
  • (论文阅读22/100)Learning a Deep Compact Image Representation for Visual Tracking
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (原創) 物件導向與老子思想 (OO)
  • (转)C#开发微信门户及应用(1)--开始使用微信接口
  • (转)为C# Windows服务添加安装程序
  • (自用)交互协议设计——protobuf序列化
  • .NET Core、DNX、DNU、DNVM、MVC6学习资料
  • .NET 表达式计算:Expression Evaluator
  • .NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】
  • .net通过类组装数据转换为json并且传递给对方接口
  • /run/containerd/containerd.sock connect: connection refused