当前位置:首页 > 文章列表 > 文章 > python教程 > Python参数传递:值传递还是引用传递?

Python参数传递:值传递还是引用传递?

2025-09-09 20:13:59 0浏览 收藏

深入理解Python参数传递机制是编写高效、健壮代码的关键。本文旨在剖析Python中“传对象引用”的本质,它既非传统的值传递,也非严格的引用传递,而是一种独特的“传赋值”方式。通过变量与对象的标签关系类比,揭示函数内外参数共享对象的底层原理。文章详细对比了可变对象(如列表、字典)和不可变对象(如字符串、元组)在参数传递中的不同表现,以及函数内部操作对外部变量的影响。同时,提供了实用的解决方案,如返回新值、创建浅拷贝或深拷贝,以及明确函数契约,帮助开发者避免因参数传递引发的意外修改,编写更可预测、易于维护的Python代码。理解Python参数传递的精髓,能有效提升代码质量和开发效率。

Python参数传递的核心机制是“传对象引用”,即传递变量所指向对象的引用。函数内外的参数共享同一对象,若对象可变(如列表),内部修改会影响外部;若不可变(如字符串),则内部重新赋值不会影响外部。

值传递还是引用传递?Python 的参数传递机制

Python的参数传递机制,既不是纯粹的“值传递”(pass by value),也不是严格意义上的“引用传递”(pass by reference)。它更准确地说是“传对象引用”(pass by object reference),或者用更直白的方式讲,就是“传赋值”(pass by assignment)。这意味着当你将一个变量作为参数传递给函数时,实际上是把这个变量所指向的“对象”的引用传递了过去,函数内部的参数名会绑定到同一个对象上。

解决方案

理解Python的参数传递机制,关键在于把握“变量是标签,对象是数据”这个核心理念。当我们将一个变量传递给函数时,实际上是将这个标签指向的内存地址(即对象的引用)传递给了函数的形参。这意味着,函数内部的形参和函数外部的实参,最初都指向内存中的同一个对象。

如果这个对象是可变的(mutable,如列表、字典、集合),那么在函数内部对这个对象进行的修改,会直接影响到函数外部的原始对象。因为它们指向的是同一个实际数据。

而如果这个对象是不可变的(immutable,如整数、浮点数、字符串、元组),那么在函数内部,即使你尝试“修改”它,Python实际上会创建一个新的对象,并将函数内部的形参重新绑定到这个新对象上。这时,函数外部的原始变量仍然指向旧的、未被改变的对象。

这个机制巧妙地结合了两种传递方式的特性,既避免了纯粹值传递带来的数据复制开销(尤其对大对象),又通过不可变对象的特性保护了原始数据不被意外修改,同时又允许通过可变对象实现函数对外部状态的直接影响。理解其精髓,能帮助我们写出更健壮、更可预测的代码。

Python参数传递的核心机制究竟是什么?

要深挖Python参数传递的本质,我们需要跳出传统编程语言中“值传递”和“引用传递”的二元对立思维。Python采取的是一种独特的“传对象引用”模式,这与它“一切皆对象”的设计哲学一脉相承。

想象一下,Python中的变量,它们不是存储数据的盒子,更像是贴在数据盒子上的标签。当你写 a = 10 时,你创建了一个值为10的整数对象,然后把标签 a 贴在了这个对象上。当你再写 b = a 时,你并没有复制10这个值,而是让标签 b 也贴在了同一个值为10的整数对象上。

当我们将 a 传递给一个函数 func(x) 时,发生的情况是:函数内部的形参 x 也被贴到了 a 原本指向的那个对象上。此刻,ax 都是指向同一个对象的标签。

def modify_value(param):
    print(f"Inside func, param starts as: {param}, id: {id(param)}")
    param = 20 # 这里是重新赋值,不是修改原始对象
    print(f"Inside func, param becomes: {param}, id: {id(param)}")

my_int = 10
print(f"Outside func, my_int starts as: {my_int}, id: {id(my_int)}")
modify_value(my_int)
print(f"Outside func, my_int after call: {my_int}, id: {id(my_int)}")

输出会清晰地展示:my_int 在函数调用前后 id 保持不变,而 param 在函数内部被重新赋值后,其 id 发生了变化。这说明 param = 20 这行代码,并没有改变 my_int 指向的那个 10 对象,而是让 param 这个标签转而去指向了一个新的 20 对象。my_int 依然坚守着它的 10

这种机制的好处是显而易见的:避免了不必要的内存复制,尤其是在处理大型数据结构时,效率优势非常明显。同时,它也要求我们对可变对象和不可变对象的行为有清晰的认知,否则很容易踩坑。

可变对象与不可变对象在参数传递中有什么不同表现?

这是Python参数传递中最常引起混淆,也最需要深入理解的地方。对象的“可变性”决定了函数内部操作对其外部影响的程度。

不可变对象(Immutable Objects): 包括整数(int)、浮点数(float)、字符串(str)、元组(tuple)、布尔值(bool)等。这些对象一旦创建,它们的值就不能被改变。如果你尝试“修改”一个不可变对象,Python实际上会创建一个新的对象,并让变量指向这个新对象。

当一个不可变对象作为参数传递时:

def change_string(s):
    print(f"Inside func, s before change: '{s}', id: {id(s)}")
    s = s + " world" # 创建了一个新字符串对象
    print(f"Inside func, s after change: '{s}', id: {id(s)}")

my_str = "hello"
print(f"Outside func, my_str before call: '{my_str}', id: {id(my_str)}")
change_string(my_str)
print(f"Outside func, my_str after call: '{my_str}', id: {id(my_str)}")

这里,my_str 始终指向最初的 "hello" 对象。函数内部的 s = s + " world" 只是让 s 这个局部标签重新指向了一个新的字符串对象 "hello world",而 my_str 毫发无损。这是因为字符串是不可变的,任何看似“修改”的操作,本质上都是创建新对象。

可变对象(Mutable Objects): 包括列表(list)、字典(dict)、集合(set)等。这些对象在创建后,它们的内容可以被修改。

当一个可变对象作为参数传递时:

def modify_list(l):
    print(f"Inside func, l before modification: {l}, id: {id(l)}")
    l.append(4) # 直接修改了列表对象的内容
    print(f"Inside func, l after modification: {l}, id: {id(l)}")
    l = [5, 6, 7] # 重新赋值,l指向了一个新列表
    print(f"Inside func, l after re-assignment: {l}, id: {id(l)}")

my_list = [1, 2, 3]
print(f"Outside func, my_list before call: {my_list}, id: {id(my_list)}")
modify_list(my_list)
print(f"Outside func, my_list after call: {my_list}, id: {id(my_list)}")

观察输出,你会发现 l.append(4) 这行代码确实改变了 my_list 的内容,因为 lmy_list 在那一刻指向的是同一个列表对象。它们的 id 相同,对其中一个的修改会反映在另一个上。然而,l = [5, 6, 7] 这行代码,就像之前不可变对象的例子一样,只是让 l 这个标签重新指向了一个全新的列表对象,这并没有影响 my_list

所以,关键点在于:是修改对象本身的内容,还是将变量重新绑定到新的对象。 前者会影响外部的可变对象,后者则不会。

如何避免函数内部修改对外部变量产生意外影响?

在处理可变对象作为参数时,我们常常希望函数能完成它的任务,但又不想它“污染”到函数外部的原始数据。这就像你把一份重要文件借给同事,你希望他阅读、分析,但绝对不希望他直接在原件上涂改。这里有几种策略可以帮助我们管理这种行为:

  1. 返回新值,而不是修改原值: 这是最推荐的做法,尤其是在函数设计时。让函数接收参数,处理数据,然后返回一个新的处理结果,而不是直接修改传入的参数。这遵循了“纯函数”的思想,提高了代码的可预测性和可测试性。

    def add_item_new_list(original_list, item):
        new_list = list(original_list) # 创建一个副本
        new_list.append(item)
        return new_list
    
    my_data = [1, 2, 3]
    processed_data = add_item_new_list(my_data, 4)
    print(f"Original data: {my_data}") # [1, 2, 3]
    print(f"Processed data: {processed_data}") # [1, 2, 3, 4]

    这种方式清晰地分离了输入和输出,避免了副作用。

  2. 创建参数的副本(Shallow Copy 或 Deep Copy): 如果你确实需要在函数内部修改一个可变对象,但又不希望影响原始对象,那么在函数一开始就创建该对象的一个副本是一个好方法。

    • 浅拷贝(Shallow Copy):对于列表,可以使用 list_param[:]list_param.copy()。对于字典,可以使用 dict_param.copy()。浅拷贝会创建一个新的容器对象,但容器内的元素仍然是原始元素的引用。如果元素本身是可变对象,那么修改这些嵌套的可变元素仍然会影响到原始对象。

      def modify_list_safely(l):
          local_list = l[:] # 浅拷贝
          local_list.append(4)
          print(f"Inside func, local_list: {local_list}")
      
      my_data = [1, 2, 3]
      modify_list_safely(my_data)
      print(f"Outside func, my_data: {my_data}") # [1, 2, 3]
    • 深拷贝(Deep Copy):当你的可变对象包含嵌套的可变对象(例如,一个列表里包含另一个列表)时,浅拷贝就不够了。你需要使用 copy 模块的 deepcopy() 函数来创建一个完全独立的副本,包括所有嵌套的子对象。

      import copy
      
      def modify_nested_list_safely(l):
          local_list = copy.deepcopy(l) # 深拷贝
          local_list[0].append('x')
          print(f"Inside func, local_list: {local_list}")
      
      my_nested_data = [[1, 2], [3, 4]]
      modify_nested_list_safely(my_nested_data)
      print(f"Outside func, my_nested_data: {my_nested_data}") # [[1, 2], [3, 4]]

      如果没有 deepcopymy_nested_data[0] 也会被修改。

  3. 明确函数契约与文档: 有时候,函数设计就是需要修改传入的可变参数,例如一个排序函数 sort_in_place(a_list)。在这种情况下,关键在于清晰地在函数文档字符串(docstring)中说明这种行为,让调用者知道传入的参数会被修改。

    def sort_in_place(data_list):
        """
        对传入的列表进行原地排序。
        注意:此函数会直接修改传入的列表对象。
        """
        data_list.sort()
    
    numbers = [3, 1, 4, 1, 5, 9]
    sort_in_place(numbers)
    print(f"Sorted numbers: {numbers}") # [1, 1, 3, 4, 5, 9]

    通过清晰的文档,可以避免用户误解函数的行为,从而减少意外的发生。选择哪种策略取决于具体的业务需求和代码设计哲学,但始终牢记可变对象和不可变对象的区别,是编写高质量Python代码的基础。

本篇关于《Python参数传递:值传递还是引用传递?》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于文章的相关知识,请关注golang学习网公众号!

HTML滑动侧边栏实现方法详解HTML滑动侧边栏实现方法详解
上一篇
HTML滑动侧边栏实现方法详解
Golangchannel阻塞与非阻塞详解
下一篇
Golangchannel阻塞与非阻塞详解
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    514次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    499次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • SEO  AI Mermaid 流程图:自然语言生成,文本驱动可视化创作
    AI Mermaid流程图
    SEO AI Mermaid 流程图工具:基于 Mermaid 语法,AI 辅助,自然语言生成流程图,提升可视化创作效率,适用于开发者、产品经理、教育工作者。
    49次使用
  • 搜获客笔记生成器:小红书医美爆款内容AI创作神器
    搜获客【笔记生成器】
    搜获客笔记生成器,国内首个聚焦小红书医美垂类的AI文案工具。1500万爆款文案库,行业专属算法,助您高效创作合规、引流的医美笔记,提升运营效率,引爆小红书流量!
    19次使用
  • iTerms:一站式法律AI工作台,智能合同审查起草与法律问答专家
    iTerms
    iTerms是一款专业的一站式法律AI工作台,提供AI合同审查、AI合同起草及AI法律问答服务。通过智能问答、深度思考与联网检索,助您高效检索法律法规与司法判例,告别传统模板,实现合同一键起草与在线编辑,大幅提升法律事务处理效率。
    57次使用
  • 迅捷AIPPT:AI智能PPT生成器,高效制作专业演示文稿
    迅捷AIPPT
    迅捷AIPPT是一款高效AI智能PPT生成软件,一键智能生成精美演示文稿。内置海量专业模板、多样风格,支持自定义大纲,助您轻松制作高质量PPT,大幅节省时间。
    43次使用
  • 迅捷AI写作软件:AI智能创作专家,赋能高效文本处理
    迅捷AI写作
    迅捷AI写作,您的智能AI写作助手!快速生成各类文稿,涵盖新媒体、工作汇报。更兼具文字识别、语音转换、格式转换等实用功能,一站式解决文本处理难题,显著提升工作效率。
    28次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码