杰瑞科技汇

Python中引用如何理解与使用?

万物皆对象,名存地址

在 Python 中,所有的数据,无论是整数、字符串、列表、函数,甚至是一个模块,都是以对象 的形式存在的,当你创建一个变量时,a = 10,你并没有在变量 a 中“存放”数字 10 本身,相反,你做的事情是:

Python中引用如何理解与使用?-图1
(图片来源网络,侵删)
  1. 在内存中创建一个代表数字 10 的对象。
  2. 创建一个名为 a 的名字(变量名)。
  3. 将名字 a 和这个内存对象的地址 关联起来。

这个关联关系,就是引用a 是一个引用,它指向了数字 10 这个对象。

我们可以用一个简单的比喻来理解:

  • 对象:是你家的一栋房子(实体)。
  • 变量名:是写在这栋房子上的门牌号(标签)。
  • 引用:是门牌号和房子之间的对应关系。

多个门牌号可以指向同一栋房子,同样,多个变量名也可以引用同一个对象。


引用的可视化与 id() 函数

Python 提供了 id() 函数,可以让我们查看一个对象在内存中的唯一标识符(可以理解为它的地址),通过观察 id() 的变化,我们可以直观地理解引用的工作方式。

Python中引用如何理解与使用?-图2
(图片来源网络,侵删)
# 示例 1:基本赋值
a = 10
print(f"a = {a}, id(a) = {id(a)}")
# 将 a 的引用赋给 b
b = a
print(f"b = {b}, id(b) = {id(b)}")
# a 和 b 指向同一个对象
print(f"a is b: {a is b}") # is 关键字检查两个引用是否指向同一个对象
# 输出:
# a = 10, id(a) = 140736688229600  (你的地址会不同)
# b = 10, id(b) = 140736688229600
# a is b: True

从上面的例子可以看出,b = a 并没有复制数字 10,而是让变量 b 也指向了 a 所指向的那个对象。


可变对象与不可变对象

这是理解引用行为时最重要的一点,也是 Python 中许多常见 Bug 的根源。

1 不可变对象

不可变对象 指的是对象一旦被创建,其内部的值就不能被修改,如果尝试修改,Python 会创建一个全新的对象。

常见的不可变对象包括:

  • 数字 (int, float, complex)
  • 字符串 (str)
  • 元组 (tuple)
  • 布尔值 (bool)
  • frozenset (冻结集合)
# 示例 2:不可变对象 (整数)
a = 10
print(f"初始: a = {a}, id(a) = {id(a)}")
# 尝试修改 a
a = a + 1  # 这行代码的实质是:1. 创建一个新对象 11; 2. 让 a 引用这个新对象
print(f"修改后: a = {a}, id(a) = {id(a)}")
# 检查 b 是否还指向原来的对象
b = 10
print(f"b = {b}, id(b) = {id(b)}") # b 仍然指向原来的对象 10
print(f"a is b: {a is b}") # False, 因为 a 现在指向 11
# 输出:
# 初始: a = 10, id(a) = 140736688229600
# 修改后: a = 11, id(a) = 140736688229632  (地址变了!)
# b = 10, id(b) = 140736688229600
# a is b: False

2 可变对象

可变对象 指的是对象在被创建后,其内部的值可以被修改,修改操作不会创建新对象,而是在原对象上进行。

常见的可变对象包括:

  • 列表 (list)
  • 字典 (dict)
  • 集合 (set)
  • 自定义类的实例
# 示例 3:可变对象 (列表)
my_list = [1, 2, 3]
print(f"初始: my_list = {my_list}, id(my_list) = {id(my_list)}")
# another_list 引用 my_list 指向的同一个列表
another_list = my_list
print(f"赋值后: another_list = {another_list}, id(another_list) = {id(another_list)}")
# 修改 my_list (注意是修改其内容,而不是重新赋值)
my_list.append(4)
print(f"my_list.append(4) 后:")
print(f"my_list = {my_list}, id(my_list) = {id(my_list)}")
print(f"another_list = {another_list}, id(another_list) = {id(another_list)}")
# another_list 也被改变了!因为它们引用的是同一个对象。
# 输出:
# 初始: my_list = [1, 2, 3], id(my_list) = 2323455663976
# 赋值后: another_list = [1, 2, 3], id(another_list) = 2323455663976
# my_list.append(4) 后:
# my_list = [1, 2, 3, 4], id(my_list) = 2323455663976  (地址没变)
# another_list = [1, 2, 3, 4], id(another_list) = 2323455663976

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

这是一个经典的面试题,Python 的函数参数传递机制是“传对象引用”(Pass-by-object-reference)。

这意味着,当你把一个变量作为参数传递给函数时,传递的是该变量所指向的对象的引用(地址),而不是变量本身或对象的副本。

这会产生两种情况,取决于对象是可变的还是不可变的。

1 传递不可变对象

函数内部无法修改原始对象,因为它不能被修改,任何“修改”操作都会创建一个新对象,并且这个新对象只在函数的局部作用域内有效。

def modify_immutable(x):
    print(f"函数内 - 修改前: x = {x}, id(x) = {id(x)}")
    x = x + 10  # 创建了一个新对象 20
    print(f"函数内 - 修改后: x = {x}, id(x) = {id(x)}")
    return x
my_var = 10
print(f"函数外 - 调用前: my_var = {my_var}, id(my_var) = {id(my_var)}")
# my_var 的引用被传递给 x
result = modify_immutable(my_var)
print(f"函数外 - 调用后: my_var = {my_var}, id(my_var) = {id(my_var)}")
print(f"函数返回值: result = {result}")
# 输出:
# 函数外 - 调用前: my_var = 10, id(my_var) = 140736688229600
# 函数内 - 修改前: x = 10, id(x) = 140736688229600
# 函数内 - 修改后: x = 20, id(x) = 140736688229936
# 函数外 - 调用后: my_var = 10, id(my_var) = 140736688229600  # my_var 未被改变
# 函数返回值: result = 20

2 传递可变对象

函数内部可以通过引用来修改原始对象的内容。

def modify_mutable(lst):
    print(f"函数内 - 修改前: lst = {lst}, id(lst) = {id(lst)}")
    lst.append(99)  # 修改了 lst 引用的列表对象
    print(f"函数内 - 修改后: lst = {lst}, id(lst) = {id(lst)}")
my_list = [1, 2, 3]
print(f"函数外 - 调用前: my_list = {my_list}, id(my_list) = {id(my_list)}")
# my_list 的引用被传递给 lst
modify_mutable(my_list)
print(f"函数外 - 调用后: my_list = {my_list}, id(my_list) = {id(my_list)}")
# 输出:
# 函数外 - 调用前: my_list = [1, 2, 3], id(my_list) = 2323455663976
# 函数内 - 修改前: lst = [1, 2, 3], id(lst) = 2323455663976
# 函数内 - 修改后: lst = [1, 2, 3, 99], id(lst) = 2323455663976
# 函数外 - 调用后: my_list = [1, 2, 3, 99], id(my_list) = 2323455663976  # my_list 被改变了

3 避免意外修改:创建副本

如果你想在函数内部使用可变对象,但又不希望修改外部的原始对象,你需要传递一个副本

def safe_function(lst):
    # 创建一个列表的副本,这样操作的就是新对象了
    local_list = lst.copy()  # 或者 lst[:]
    local_list.append(99)
    print(f"函数内: local_list = {local_list}")
my_list = [1, 2, 3]
safe_function(my_list)
print(f"函数外: my_list = {my_list}") # my_list 保持不变
# 输出:
# 函数内: local_list = [1, 2, 3, 99]
# 函数外: my_list = [1, 2, 3]

浅拷贝与深拷贝

当你确实需要复制一个对象时,需要区分浅拷贝和深拷贝。

1 浅拷贝

copy.copy() 或者 list.copy() / dict.copy() 等方法创建的是浅拷贝

  • 对于不可变对象,浅拷贝和直接赋值效果一样,都是引用同一个对象。
  • 对于可变对象
    • 它会创建一个新的容器对象(比如一个新的列表)。
    • 这个新容器中的元素仍然是引用了原来对象中的元素
# 示例 4: 浅拷贝
original_list = [1, [2, 3], 4]
# 创建浅拷贝
shallow_copy_list = original_list.copy()
print(f"original_list: {original_list}, id: {id(original_list)}")
print(f"shallow_copy_list: {shallow_copy_list}, id: {id(shallow_copy_list)}")
print(f"original_list[0] is shallow_copy_list[0]: {original_list[0] is shallow_copy_list[0]}") # True (不可变元素)
print(f"original_list[1] is shallow_copy_list[1]: {original_list[1] is shallow_copy_list[1]}") # True (可变元素)
# 修改浅拷贝中的不可变元素
shallow_copy_list[0] = 100
print("\n修改不可变元素后:")
print(f"original_list: {original_list}") # 不受影响
print(f"shallow_copy_list: {shallow_copy_list}")
# 修改浅拷贝中的可变元素
shallow_copy_list[1].append(99)
print("\n修改可变元素后:")
print(f"original_list: {original_list}") # 受到影响!因为内部列表是同一个
print(f"shallow_copy_list: {shallow_copy_list}")
# 输出:
# original_list: [1, [2, 3], 4], id: 2323455664120
# shallow_copy_list: [1, [2, 3], 4], id: 2323455664152
# original_list[0] is shallow_copy_list[0]: True
# original_list[1] is shallow_copy_list[1]: True
# 修改不可变元素后:
# original_list: [1, [2, 3], 4]
# shallow_copy_list: [100, [2, 3], 4]
# 修改可变元素后:
# original_list: [1, [2, 3, 99], 4]
# shallow_copy_list: [100, [2, 3, 99], 4]

2 深拷贝

copy.deepcopy() 创建的是深拷贝

它会递归地创建所有嵌套对象的副本,确保新对象和原对象没有任何共享的部分,修改深拷贝后的任何部分都不会影响原对象。

# 示例 5: 深拷贝
import copy
original_list = [1, [2, 3], 4]
# 创建深拷贝
deep_copy_list = copy.deepcopy(original_list)
print(f"original_list: {original_list}")
print(f"deep_copy_list: {deep_copy_list}")
print(f"original_list[1] is deep_copy_list[1]: {original_list[1] is deep_copy_list[1]}") # False
# 修改深拷贝中的可变元素
deep_copy_list[1].append(99)
print("\n修改可变元素后:")
print(f"original_list: {original_list}") # 不受影响!
print(f"deep_copy_list: {deep_copy_list}")
# 输出:
# original_list: [1, [2, 3], 4]
# deep_copy_list: [1, [2, 3], 4]
# original_list[1] is deep_copy_list[1]: False
# 修改可变元素后:
# original_list: [1, [2, 3], 4]
# deep_copy_list: [1, [2, 3, 99], 4]

概念 解释 关键点
引用 变量名是对象的“标签”,引用是标签与对象内存地址的关联。 a = 10a 是一个引用。
不可变对象 不能改变,修改操作会创建新对象。 int, str, tuple,函数内修改不影响外部变量。
可变对象 可以改变,修改操作在原对象上进行。 list, dict, set,函数内修改会影响外部变量。
函数传参 传的是对象的引用(地址)。 传不可变对象:安全,传可变对象:有副作用,需小心。
浅拷贝 创建新容器,但容器内的元素仍是原对象的引用。 修改嵌套的可变元素会影响原对象。
深拷贝 递归创建所有嵌套对象的完全独立副本。 新旧对象完全独立,互不影响。

理解引用是 Python 编程的基石,当你遇到一个变量在函数调用后“意外”改变,或者当你想要复制一个列表却发现只复制了第一层时,问题通常都出在对引用、可变/不可变对象以及拷贝方式的理解上。

分享:
扫描分享到社交APP
上一篇
下一篇