杰瑞科技汇

Python aliasing用法是什么?

这个概念虽然简单,但对于理解 Python 的工作方式(特别是处理可变对象时)至关重要。

Python aliasing用法是什么?-图1
(图片来源网络,侵删)

什么是别名?

别名就是为一个已经存在的对象创建了一个新的名称(引用)

在 Python 中,你几乎总是在操作对象的引用,而不是对象本身,当你把一个变量赋值给另一个变量时,你并没有创建一个新对象,而是让第二个变量指向了第一个变量所指向的那个对象。

这就像你的名字,你可能有多个昵称(比如大名“张三”,小名“小三”),但它们都指向同一个人。


核心原理:对象与引用

理解别名之前,必须先理解 Python 的内存模型:

Python aliasing用法是什么?-图2
(图片来源网络,侵删)
  • 对象:存储在内存中的数据,10"hello"[1, 2, 3]
  • 引用:可以理解为指向对象的“指针”或“标签”,变量就是存储这些引用的名称。

示例 1:不可变对象(如整数、字符串)

# a 绑定到整数对象 10
a = 10
# b 成为了 a 的别名,它们指向同一个对象 10
b = a
print(f"a 的 id: {id(a)}")  # 输出 a 的内存地址
print(f"b 的 id: {id(b)}")  # 输出 b 的内存地址
print(f"a == b? {a == b}")  # 值是否相等
print(f"a is b? {a is b}")  # 是否是同一个对象 (id 是否相同)
# 输出结果 (id 可能不同,但 a is b 会是 True)
# a 的 id: 140734668391968
# b 的 id: 140734668391968
# a == b? True
# a is b? True
# 现在修改 a
a = 20
# 因为整数是不可变的,修改 a 实际上是让 a 指向了一个全新的对象 20
# b 仍然指向原来的对象 10
print(f"\n修改 a 后:")
print(f"a = {a}")  # 输出 20
print(f"b = {b}")  # 输出 10
print(f"a 的 id: {id(a)}")  # id 发生了变化
print(f"b 的 id: {id(b)}")  # id 保持不变

对于不可变对象,创建别名看起来很简单,但修改其中一个不会影响另一个,因为“修改”实际上是创建了新对象。


别名的主要问题:可变对象

这才是别名需要重点关注的地方,因为它会带来意想不到的副作用。

示例 2:可变对象(如列表、字典)

Python aliasing用法是什么?-图3
(图片来源网络,侵删)
# my_list 是一个列表对象
my_list = [1, 2, 3]
# new_list 成为了 my_list 的别名,它们指向同一个列表对象
new_list = my_list
print(f"my_list 的 id: {id(my_list)}")
print(f"new_list 的 id: {id(new_list)}")
print(f"my_list is new_list? {my_list is new_list}") # True
# 通过 new_list 修改列表内容
# 因为列表是可变的,我们并没有创建新列表,而是直接修改了它所指向的对象
new_list.append(4)
print("\n通过 new_list 添加元素后:")
print(f"my_list = {my_list}")  # 输出 [1, 2, 3, 4]  !!! my_list 也被改变了
print(f"new_list = {new_list}") # 输出 [1, 2, 3, 4]

问题所在:当你通过一个别名修改可变对象时,所有指向该对象的别名都会受到影响,这常常是程序中出现 bug 的根源,因为你以为在操作两个独立的列表,实际上它们是同一个。


如何避免或利用别名?

1 避免意外的副作用:创建副本

当你想拥有一个独立于原始对象的副本时,需要明确地创建它。

对于列表:

  • 切片操作 [:]:最常用、最 Pythonic 的方式。
  • list() 构造函数
  • copy.copy():浅拷贝。
original_list = [1, 2, ['a', 'b']] # 包含嵌套列表
# 方法1: 切片创建副本
copy_list_1 = original_list[:]
# 方法2: list() 构造函数创建副本
copy_list_2 = list(original_list)
# 方法3: copy 模块创建浅拷贝
import copy
copy_list_3 = copy.copy(original_list)
# 修改副本
copy_list_1.append(3)
copy_list_2[0] = 99
# 浅拷贝只拷贝第一层,嵌套列表仍然是共享的别名
copy_list_3[2].append('c') # 这会影响 original_list
print(f"original_list: {original_list}") # 输出 [1, 2, ['a', 'b', 'c']]
print(f"copy_list_1:   {copy_list_1}")   # 输出 [1, 2, ['a', 'b'], 3]
print(f"copy_list_2:   {copy_list_2}")   # 输出 [99, 2, ['a', 'b']]
print(f"copy_list_3:   {copy_list_3}")   # 输出 [1, 2, ['a', 'b', 'c']]

对于字典:

  • dict() 构造函数
  • copy.copy():浅拷贝。
  • copy.deepcopy():深拷贝(解决嵌套对象问题)。
import copy
original_dict = {'name': 'Alice', 'scores': [88, 92]}
# 浅拷贝
shallow_copy = original_dict.copy()
# 深拷贝
deep_copy = copy.deepcopy(original_dict)
# 修改浅拷贝中的可变对象
shallow_copy['scores'].append(95)
# 修改深拷贝
deep_copy['name'] = 'Bob'
print(f"original_dict: {original_dict}") # 输出 {'name': 'Alice', 'scores': [88, 92, 95]}
print(f"shallow_copy:  {shallow_copy}")  # 输出 {'name': 'Alice', 'scores': [88, 92, 95]}
print(f"deep_copy:     {deep_copy}")     # 输出 {'name': 'Bob', 'scores': [88, 92]}

浅拷贝 vs 深拷贝

  • 浅拷贝:创建一个新对象,但如果对象内部包含其他可变对象(如列表、字典),则这些内部对象仍然是原对象的别名。
  • 深拷贝:创建一个完全独立的新对象,递归地拷贝所有内部对象,确保新对象与原对象没有任何共享部分。

2 利用别名:提高效率

别名并非总是坏事,它也可以被巧妙地利用。

  • 避免不必要的对象复制:当你需要在函数或代码的不同部分操作同一个大对象时,传递别名比复制整个对象要高效得多,尤其是在处理大型数据结构(如 NumPy 数组、Pandas DataFrame)时。

  • 简化代码:如果一个对象很长很复杂,给它一个短别名可以让代码更清晰。

import pandas as pd
# 假设 df 是一个非常大的 DataFrame
df = pd.read_csv('very_large_file.csv')
# 创建一个别名,使代码更简洁
df_processed = df
# 现在可以使用 df_processed 来操作,它和 df 是同一个对象
df_processed.dropna(inplace=True)
df_processed['new_column'] = df_processed['old_column'] * 2
# 这些修改会直接反映在 df 上
print(df.head()) # df 也被修改了

函数参数传递与别名

Python 中函数参数的传递机制是“赋值传递”,本质上就是传递引用

  • 如果传递的是不可变对象,函数内对参数的修改不会影响外部的变量。
  • 如果传递的是可变对象,函数内通过这个引用修改了对象,外部的原始对象也会被改变。
def modify_immutable(x):
    x = 100  # x 成为一个指向新对象 100 的引用,不影响外部的 a
    print(f"函数内部 x 的 id: {id(x)}")
def modify_mutable(lst):
    lst.append(99)  # 通过 lst 这个引用修改了它指向的列表对象
    print(f"函数内部 lst 的 id: {id(lst)}")
a = 10
my_list = [1, 2, 3]
print(f"调用函数前 a 的 id: {id(a)}")
modify_immutable(a)
print(f"调用函数后 a = {a}") # a 仍然是 10
print("\n---\n")
print(f"调用函数前 my_list 的 id: {id(my_list)}")
modify_mutable(my_list)
print(f"调用函数后 my_list = {my_list}") # my_list 变成了 [1, 2, 3, 99]

特性 不可变对象 (int, str, tuple) 可变对象 (list, dict, set)
别名创建 b = aba 的别名 b = aba 的别名
修改别名 a = new_vala 指向新对象,b 不变 a.append(item)ab 都指向被修改后的同一个对象
副作用 !这是最常见的 bug 来源
如何独立操作 直接修改即可 必须创建副本(如 [:], copy.deepcopy()
函数参数 函数内修改不影响外部 函数内修改会影响外部原始对象

核心要点

  1. 理解 is vs is 检查两个引用是否指向同一个对象(id 是否相同)。 检查两个对象的值是否相等,在处理别名时,is 更能揭示真相。
  2. 警惕可变对象:当你把一个列表或字典赋值给另一个变量时,要时刻记住它们现在是“同一个人”。
  3. 明确意图:如果你需要一个独立的副本,就使用 [:]copy.deepcopy;如果你希望共享同一个对象以提高效率,就直接使用别名。
  4. 函数参数是别名:传递给函数的参数是外部对象的别名,修改可变对象会直接影响外部。
分享:
扫描分享到社交APP
上一篇
下一篇