1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选

前面我们学习了 Python 的变量作用域,了解了局部变量、全局变量,以及 global 和 nonlocal 的使用方法。

但还有一个问题值得深入研究:当我们把一个变量传递给函数时,函数拿到的究竟是什么?

为什么把列表传给函数,在函数内部添加一个元素,外面的列表也跟着变了?而把整数传给函数,在函数内部进行加法运算,外面的整数却没有变化?

难道 Python 对不同的数据类型,采用了不同的参数传递机制?

这篇文章就来把这个问题讲清楚。理解了变量、对象和引用之间的关系,后面学习列表、字典、函数以及 map()、filter() 等知识时,就会轻松很多。

变量传递给函数时,到底发生了什么?

先来看两个非常简单的例子。

第一个例子,传递一个整数:

1
2
3
4
5
6
7
8
9
def modify_number(number):
number = number + 1
print(f"函数内部:{number}")


x = 10
modify_number(x)

print(f"函数外部:{x}")

运行结果:

1
2
函数内部:11
函数外部:10

函数内部的 number 已经变成了 11,为什么外面的 x 还是 10?

再来看第二个例子,传递一个列表:

1
2
3
4
5
6
7
8
9
def modify_list(numbers):
numbers.append(4)
print(f"函数内部:{numbers}")


nums = [1, 2, 3]
modify_list(nums)

print(f"函数外部:{nums}")

运行结果:

1
2
函数内部:[1, 2, 3, 4]
函数外部:[1, 2, 3, 4]

这一次,函数内部的操作却影响了外部的列表。

两个例子看起来非常相似,结果却完全不同。

如果只记住“整数是不可变对象,列表是可变对象”,虽然能够解释一部分现象,却还没有真正理解背后的原因。

想要彻底弄明白,我们需要先认识三个概念:变量、对象和引用。

变量、对象和引用是什么关系?

在 Python 中,我们经常说变量保存了一个值,但更准确地理解,变量名是一个用于引用对象的名称。

例如:

1
server_name = "web-01"

这条语句可以理解为:让变量名 server_name 绑定到字符串对象 "web-01"。

再来看一个例子:

1
cpu_usage = 75

这里,变量名 cpu_usage 绑定到整数对象 75。

可以用下面这张图来理解它们之间的关系。

flowchart TD
    A["变量名 server_name"] --> B["字符串对象 web-01"]
    C["变量名 cpu_usage"] --> D["整数对象 75"]

这里需要注意:变量名和对象不是同一个东西。

变量名相当于一个标识,可以让程序找到对应的对象。多个变量名也可以同时绑定到同一个对象。

例如:

1
2
3
4
5
6
server_a = [1, 2, 3]
server_b = server_a

print(server_a)
print(server_b)
print(server_a is server_b)

运行结果:

1
2
3
[1, 2, 3]
[1, 2, 3]
True

server_a is server_b 的结果是 True,说明两个变量名引用的是同一个对象。

如果我们通过 server_b 修改列表:

1
2
3
4
server_b.append(4)

print(server_a)
print(server_b)

运行结果:

1
2
[1, 2, 3, 4]
[1, 2, 3, 4]

为什么修改 server_b 后,server_a 看到的内容也发生了变化?

原因很简单:它们指向同一个列表对象。修改这个对象后,通过任何一个仍然引用该对象的变量名访问,都能看到修改后的内容。

可以用下面的示意图表示:

flowchart TD
    A["server_a"] --> C["同一个列表对象"]
    B["server_b"] --> C
    C --> D["[1, 2, 3, 4]"]

这就是理解 Python 函数参数传递的重要基础。

Python 函数参数采用什么传递机制?

当调用函数时,Python 会将实参所引用的对象传递给函数,并让形参在函数内部绑定到该对象。

这种机制通常称为按对象引用传递,也常称为共享传参(Call by Sharing)。

例如:

1
2
3
4
5
6
def check_server(server):
print(server)


server_name = "web-01"
check_server(server_name)

返回web-01

调用 check_server(server_name) 时,函数中的形参 server 会绑定到与 server_name 相同的字符串对象。

可以将这个过程理解为:

flowchart TD
    A["外部变量 server_name"] --> C["字符串对象 web-01"]
    B["函数形参 server"] --> C

注意,这并不意味着 Python 把外部变量 server_name 本身交给了函数,也不意味着函数获得了一个自动复制出来的独立对象。

函数中的 server 是另一个变量名,它与外部的 server_name 可以引用同一个对象,但两者是不同的名称绑定。

因此,函数内部究竟会不会影响外部,关键要看函数执行的是哪种操作:

  • 修改对象本身:如果对象允许修改,外部通过该对象访问时也能看到变化。
  • 让形参重新绑定到另一个对象:这只会改变函数内部形参的绑定,不会自动改变外部变量的绑定。

接下来,我们分别看看这两种情况。

不可变对象:为什么重新赋值不会影响外部?

Python 中常见的不可变对象包括:

  • 整数,例如 10
  • 浮点数,例如 3.14
  • 布尔值,例如 True
  • 字符串,例如 "Linux"
  • 元组,例如 (1, 2, 3)

所谓不可变,指的是对象创建之后,其自身的内容不能被原地修改。

需要注意,不可变不代表变量名不能重新赋值。变量名完全可以重新绑定到另一个对象。

来看一个整数的例子:

1
2
3
4
5
6
7
8
9
def modify_number(number):
number = number + 1
print(f"函数内部:{number}")


x = 10
modify_number(x)

print(f"函数外部:{x}")

运行结果:

1
2
函数内部:11
函数外部:10

这个过程可以拆成几个步骤。

首先,执行:

1
x = 10

变量名 x 绑定到整数对象 10。

接着调用:

1
modify_number(x)

函数形参 number 也绑定到整数对象 10。

此时可以简化理解为:

flowchart TD
    A["x"] --> C["整数对象 10"]
    B["number"] --> C

然后执行:

1
number = number + 1

这里的 number + 1 会计算出结果 11,而不是把整数对象 10 原地修改为 11。

接下来,赋值操作让 number 绑定到整数对象 11。

此时,两个变量名的绑定关系变成了:

flowchart TD
    A["x"] --> B["整数对象 10"]
    C["number"] --> D["整数对象 11"]

外部的 x 仍然绑定到整数对象 10,所以它的值没有变化。

真正的关键不是函数不能修改外部变量,而是这里的赋值只改变了局部形参 number 的绑定关系。

字符串也是同样的道理。

1
2
3
4
5
6
7
8
9
def modify_name(name):
name = name + "-prod"
print(f"函数内部:{name}")


server_name = "web-01"
modify_name(server_name)

print(f"函数外部:{server_name}")

运行结果:

1
2
函数内部:web-01-prod
函数外部:web-01

字符串拼接会得到新的字符串结果,然后让局部变量 name 绑定到这个结果。外部的 server_name 并没有被重新赋值。

所以,对于不可变对象,常见的运算往往会产生一个新对象,并让变量名绑定到结果对象,而不是直接修改原对象。

可变对象:为什么修改列表会影响外部?

Python 中常见的可变对象包括:

  • 列表 list
  • 字典 dict
  • 集合 set

可变对象允许在对象存在期间修改其内容。

例如,列表可以添加、删除或替换元素,字典可以新增、删除或修改键值对。

先看列表:

1
2
3
4
5
6
7
8
def modify_list(numbers):
numbers.append(4)


nums = [1, 2, 3]
modify_list(nums)

print(nums)

运行结果:

1
[1, 2, 3, 4]

为什么外部的列表发生了变化?

调用函数时,形参 numbers 与外部变量 nums 绑定到同一个列表对象。

执行:

1
numbers.append(4)

不是让 numbers 重新绑定到一个新列表,而是直接修改它所引用的列表对象,在列表末尾添加一个元素。

因此,函数执行结束后,外部变量 nums 仍然引用原来的列表,只不过列表的内容已经改变。

可以将这个过程理解为:

flowchart LR
    A["函数调用前"] --> B["nums 和 numbers 引用同一列表"]
    B --> C["执行 numbers.append(4)"]
    C --> D["修改原列表对象"]
    D --> E["函数结束,nums 仍然引用该列表"]
    E --> F["外部看到 [1, 2, 3, 4]"]

字典也是如此。

假设我们需要记录一台 Linux 服务器的监控信息,可以将服务器状态保存到字典中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def update_server(server):
server["cpu_usage"] = 85
server["status"] = "warning"


server_info = {
"name": "web-01",
"cpu_usage": 45,
"status": "normal"
}

update_server(server_info)

print(server_info)

运行结果:

1
{'name': 'web-01', 'cpu_usage': 85, 'status': 'warning'}

函数内部修改了字典对象中的两个键值对,外部的 server_info 也能看到变化。

因为 server 和 server_info 引用的是同一个字典对象,所以通过 server 修改字典内容,就相当于修改了外部变量所引用的对象。

不过,这里有一个很重要的区别:

可变对象可以被修改,不代表函数内部对形参的所有操作都会影响外部变量。

如果函数只是让形参重新绑定到另一个列表,结果就完全不同了。

修改对象和重新赋值,有什么区别?

这是本文最值得掌握的知识点。

我们继续使用列表来演示。

先看第一种情况:修改原列表。

1
2
3
4
5
6
7
8
def modify_list(numbers):
numbers.append(4)


nums = [1, 2, 3]
modify_list(nums)

print(nums)

运行结果:

1
[1, 2, 3, 4]

这里的 append() 修改了原来的列表对象,因此外部能看到变化。

再看第二种情况:重新给形参赋值。

1
2
3
4
5
6
7
8
9
def modify_list(numbers):
numbers = [7, 8, 9]
print(f"函数内部:{numbers}")


nums = [1, 2, 3]
modify_list(nums)

print(f"函数外部:{nums}")

运行结果:

1
2
函数内部:[7, 8, 9]
函数外部:[1, 2, 3]

这一次,为什么外部的列表没有变化?

调用函数时,numbers 和 nums 原本绑定到同一个列表对象:

flowchart TD
    A["nums"] --> C["原列表 [1, 2, 3]"]
    B["numbers"] --> C

执行:

1
numbers = [7, 8, 9]

Python 创建了一个新的列表对象,并让 numbers 绑定到新对象。

原来的 nums 并没有被重新赋值,因此仍然引用 [1, 2, 3]。

此时的关系变成了:

flowchart TD
    A["nums"] --> B["原列表 [1, 2, 3]"]
    C["numbers"] --> D["新列表 [7, 8, 9]"]

两个变量名不再引用同一个列表。

这里可以记住一个非常实用的对比:

操作是否修改原对象外部能否看到对象内容变化
numbers.append(4)是能
numbers[0] = 100是能
numbers.clear()是能
numbers = [7, 8, 9]否,重新绑定形参原列表不变
numbers = numbers + [4]通常是创建新列表并重新绑定原列表不变

最后一行尤其值得注意。

对于列表,numbers + [4] 会创建一个新的列表,而 numbers += [4] 通常会原地扩展列表。对于可变对象,增强赋值运算符 += 可能修改原对象;具体行为取决于对象类型的实现,不能一概而论。

判断函数会不会影响外部,不要只看数据类型,还要看具体执行的是修改对象,还是重新绑定名称。

不可变对象也可以在函数内部重新绑定

有些初学者会误以为,不可变对象不能在函数内部修改,所以变量名也不能变化。

其实并不是这样。

来看一个字符串的例子:

1
2
3
4
5
6
7
8
9
def update_name(name):
name += "-prod"
print(f"函数内部:{name}")


server_name = "web-01"
update_name(server_name)

print(f"函数外部:{server_name}")

运行结果:

1
2
函数内部:web-01-prod
函数外部:web-01

字符串的 += 运算会产生新的字符串结果,并让局部变量 name 绑定到结果对象。它并没有修改原来的字符串对象,也没有重新绑定外部的 server_name。

同样,对于整数:

1
2
3
4
5
6
7
8
9
def add_cpu_usage(usage):
usage += 10
print(f"函数内部:{usage}")


cpu_usage = 50
add_cpu_usage(cpu_usage)

print(f"函数外部:{cpu_usage}")

运行结果:

1
2
函数内部:60
函数外部:50

这也说明,函数参数的传递机制并没有因为参数是整数还是列表而改变。

Python 使用的是同一种参数传递机制,最终表现不同,是因为对象是否可变,以及函数内部究竟执行了什么操作。

== 和 is 有什么区别?

前面我们使用过:

1
server_a is server_b

这里顺便补充一个非常重要的知识点:== 和 is 并不是一回事。

  • == 用来比较两个对象的值是否相等。
  • is 用来判断两个变量名是否引用同一个对象。

来看一个例子:

1
2
3
4
5
server_a = [1, 2, 3]
server_b = [1, 2, 3]

print(server_a == server_b)
print(server_a is server_b)

运行结果:

1
2
True
False

为什么会出现这样的结果?

因为两个列表的内容相同,所以 == 返回 True。

但是它们是分别创建的两个列表对象,因此 is 返回 False。

这与前面共享同一个列表的例子形成了对比:

1
2
3
4
5
server_a = [1, 2, 3]
server_b = server_a

print(server_a == server_b)
print(server_a is server_b)

运行结果:

1
2
True
True

理解这两个运算符,有助于我们分析函数参数传递、对象共享和对象修改等问题。

日常业务代码中,判断值是否相等通常使用 ==。判断对象身份时才使用 is,例如判断某个值是否为 None:

1
2
3
4
result = None

if result is None:
print("没有获取到结果")

不要使用 is 来代替普通的数值或字符串相等比较。

实际开发中应该怎么处理函数参数?

理解参数传递机制之后,我们就可以把这些知识应用到实际开发中了。

假设我们需要检查多台服务器,并记录检查结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def check_server(server):
server["status"] = "checked"
server["cpu_usage"] = 35


server_info = {
"name": "web-01",
"status": "unknown",
"cpu_usage": 0
}

check_server(server_info)

print(server_info)

运行结果:

1
{'name': 'web-01', 'status': 'checked', 'cpu_usage': 35}

这种写法可以工作,但函数会直接修改传入的字典。如果其他代码也在使用这个字典,就需要考虑这些修改会不会产生意外影响。

如果希望函数返回处理结果,而不修改原始字典,可以创建一份浅拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def check_server(server):
result = server.copy()
result["status"] = "checked"
result["cpu_usage"] = 35
return result


server_info = {
"name": "web-01",
"status": "unknown",
"cpu_usage": 0
}

checked_server = check_server(server_info)

print(server_info)
print(checked_server)

运行结果:

1
2
{'name': 'web-01', 'status': 'unknown', 'cpu_usage': 0}
{'name': 'web-01', 'status': 'checked', 'cpu_usage': 35}

这一次,原始字典保持不变,函数返回了新的字典。

这里的 copy() 是浅拷贝。如果字典内部还包含列表或其他可变对象,复制后的字典与原字典仍可能共享这些嵌套对象。如果需要隔离嵌套对象的修改,就需要根据实际数据结构考虑更合适的复制方式,例如 copy.deepcopy()。

在实际开发中,可以根据函数的职责选择不同的设计方式:

  • 如果函数负责更新传入的对象,可以明确约定它会修改对象。
  • 如果函数负责计算结果,可以通过 return 返回新结果,减少对外部状态的影响。
  • 如果函数不应该修改原始数据,可以根据需要创建副本。
  • 如果多个函数共享同一个可变对象,要特别留意一个函数的修改是否会影响其他代码。

对于 Linux 运维自动化脚本,这一点同样重要。例如,多个函数共享一份服务器清单时,如果某个函数意外清空了列表,后续的检查任务可能就没有服务器可以处理了。

还有一个常见陷阱:可变的默认参数

在理解了可变对象之后,还有一个 Python 初学者很容易踩到的坑:使用列表或字典作为函数的默认参数。

例如:

1
2
3
4
5
6
7
8
def add_server(server, servers=[]):
servers.append(server)
return servers


print(add_server("web-01"))
print(add_server("web-02"))
print(add_server("web-03"))

运行结果:

1
2
3
['web-01']
['web-01', 'web-02']
['web-01', 'web-02', 'web-03']

为什么第二次调用时,结果中还保留着第一次添加的 "web-01"?

因为默认参数表达式通常在函数定义时求值,而不是每次调用函数时重新创建一个列表。因此,这几次调用共享同一个默认列表对象。

每次调用 add_server() 时,函数都在修改这个列表,所以之前的内容会保留下来。

更稳妥的写法是使用 None 作为默认值,在函数内部创建新列表:

1
2
3
4
5
6
7
8
9
10
11
def add_server(server, servers=None):
if servers is None:
servers = []

servers.append(server)
return servers


print(add_server("web-01"))
print(add_server("web-02"))
print(add_server("web-03"))

运行结果:

1
2
3
['web-01']
['web-02']
['web-03']

这样,每次没有传入 servers 参数时,函数都会创建一个新的列表,不会意外共享之前调用留下的数据。

这个问题本质上也与对象和引用有关:默认参数中的列表是一个可变对象,多次函数调用反复修改了同一个对象。

写在最后

这篇文章我们从变量、对象和引用的关系出发,理解了 Python 函数参数传递的基本机制。

最后,再记住几个核心知识点:

  • Python 函数参数采用共享传参(Call by Sharing)机制。调用函数时,形参会绑定到实参所引用的对象。函数内部可以修改可变对象的内容,但如果只是让形参重新绑定到另一个对象,并不会自动改变外部变量的绑定关系。
  • 多个变量名可以引用同一个对象。
  • 可变对象可以被原地修改,修改结果可能被外部代码观察到。
  • 函数内部让形参重新绑定到另一个对象,不会自动改变外部变量的绑定。
  • 不可变对象不能原地修改,但变量名仍然可以重新绑定到其他对象。
  • 使用可变对象作为默认参数时,要特别注意对象可能在多次调用之间被共享。

理解这些内容后,我们就能更准确地解释:为什么列表在函数内部被修改后,外部也会发生变化;为什么整数在函数内部重新赋值后,外部却没有变化。

下一篇,我们将继续学习 Python 的 map() 和 filter(),看看如何用它们对列表等可迭代对象进行转换与筛选,让数据处理代码更加简洁。