Python赋值语句深度解析:从变量引用到深浅拷贝的完整指南
2026/7/30 5:18:41 网站建设 项目流程

1. 项目概述:为什么赋值语句值得你花时间深究?

看到这个标题,你可能会想:“赋值?不就是a = b吗?这有什么好讲的?” 我刚开始学Python的时候也是这么想的,直到后来在项目中踩了无数个坑,调试了几个小时才发现问题出在一个看似简单的赋值操作上,才意识到这潭水有多深。赋值语句是Python编程的基石,是每一行代码都离不开的操作。但恰恰因为太基础,很多人,包括一些有经验的开发者,都只停留在“等号右边给左边”的浅层理解上,对背后的内存模型、对象引用、可变与不可变类型之间的差异一知半解。这直接导致了代码中潜伏着难以察觉的Bug,比如列表的意外修改、函数参数的副作用、浅拷贝带来的数据污染等等。

这篇文章,我想从一个多年Python开发者的视角,和你彻底聊透赋值语句。我们不止要会写a = b,更要明白当你写下这行代码时,Python解释器在内存里为你做了什么。我们会从最基础的变量与对象关系讲起,深入到可变对象的“陷阱”,再探讨Python提供的各种“赋值”变体(序列解包、增强赋值、海象运算符),最后手把手带你掌握如何正确地“复制”一个对象。理解这些,是你写出健壮、高效、可维护Python代码的关键一步,能让你在调试时一眼看穿问题本质,在设计数据结构时做出明智选择。无论你是刚入门的新手,还是想巩固基础的中级开发者,我相信接下来的内容都能让你有新的收获。

2. 核心概念拆解:变量、对象与引用

在深入各种赋值技巧之前,我们必须统一认知:在Python中,变量不是盒子,而是标签(或称之为名字、引用)。这是理解一切赋值行为的基础。

2.1 “贴标签”模型:变量如何关联对象

想象一下,你有一个苹果(对象),你拿了一个标签贴在上面,标签上写着“fruit”(变量名)。这个“fruit”标签就指向了这个苹果。现在,你再拿一个标签“my_fruit”贴到同一个苹果上。那么,无论是通过fruit还是my_fruit,你指的都是同一个苹果。

用代码来演示这个模型:

a = [1, 2, 3] # 创建一个列表对象[1,2,3],并贴上标签`a` b = a # 将标签`b`也贴到同一个列表对象上 print(a is b) # 输出: True。`is`运算符检查两个变量是否指向同一个对象。 print(id(a), id(b)) # 输出两个相同的内存地址,这是对象在内存中的“身份证号”。

这里的关键是b = a并没有创建一个新的列表。它只是让变量b成为了同一个列表对象的另一个名字。所以,无论通过a还是b去修改这个列表,效果都是同步的:

b.append(4) print(a) # 输出: [1, 2, 3, 4] print(b) # 输出: [1, 2, 3, 4]

注意:很多初学者会误以为b = a是复制,尤其是在处理列表、字典时,这常常是Bug的源头。记住,这只是多了一个指向同一对象的引用。

2.2 可变对象 vs 不可变对象:赋值行为的分水岭

Python中的所有数据都是对象,但对象分为“可变”和“不可变”两大类。这个特性直接决定了赋值和修改时的行为差异。

不可变对象:对象创建后,其内部状态(值)就不能被改变。尝试“修改”它,Python实际上会创建一个新的对象。

  • 常见类型:int,float,str,tuple,frozenset,bytes
  • 行为示例:
x = 10 # 变量x指向整数对象10 y = x # 变量y也指向同一个整数对象10 print(x is y) # True x = x + 5 # 注意!这里不是修改了对象10,而是计算15,创建了新对象15,然后让x指向它。 print(x) # 15 print(y) # 10。y仍然指向原来的对象10。 print(x is y) # False。x和y现在指向不同的对象。

可变对象:对象创建后,其内部状态可以被修改,而对象本身的身份(内存地址)不变。

  • 常见类型:list,dict,set, 用户自定义的类实例(通常)。
  • 行为示例(接前面的列表例子):
list_a = [1, 2] list_b = list_a # list_b和list_a指向同一个列表对象 list_a.append(3) # 通过list_a修改了这个共享的列表对象 print(list_b) # [1, 2, 3]。list_b看到的也是被修改后的对象。

理解可变与不可变,是预测代码行为、避免副作用的关键。在函数传参、数据拷贝时,这一点尤为重要。

2.3id()is运算符:窥探内存的利器

当你想确认两个变量是否真的指向同一个对象,而不是值相等时,就需要用到这两个工具。

  • id(object):返回对象的“身份”,即对象在内存中的地址(通常以整数表示)。对于同一个对象,在其生命周期内,id()返回值是唯一的。
  • is运算符:用于判断两个变量引用的对象是否是同一个(即id()是否相同)。a is b等价于id(a) == id(b)
  • ==运算符:用于判断两个对象的值是否相等。它调用的是对象的__eq__()方法。

对比演示:

list1 = [1, 2, 3] list2 = [1, 2, 3] # 创建了一个值相同但完全不同的列表对象 list3 = list1 # list3和list1指向同一个对象 print(list1 == list2) # True,值相等 print(list1 is list2) # False,不是同一个对象 print(id(list1), id(list2)) # 两个不同的内存地址 print(list1 is list3) # True,是同一个对象

实操心得:在代码中,判断一个变量是否为None时,永远使用is Noneis not None,而不是== None。因为None在Python中是一个单例对象,is判断更准确、更符合习惯,且速度更快。

3. 基础赋值与增强赋值

掌握了核心模型后,我们来看看Python中几种基础的赋值形式。

3.1 简单赋值:=

这就是我们最熟悉的name = expression。它的执行顺序是:先计算等号右边的表达式,得到一个对象,然后将左边的变量名作为标签,贴到这个对象上。如果这个变量名之前已经贴过其他对象,那么旧标签会被撕下(旧对象的引用计数减1),贴到新对象上。

count = 0 # 变量count指向整数0 count = 10 # 现在count指向新的整数10,整数0如果没有其他引用,稍后会被垃圾回收 result = count * 2 + 5 # 先计算表达式(10*2+5)=25,然后让result指向25

3.2 增强赋值运算符:+=,-=,*=,/=, 等

增强赋值运算符(如+=)是“运算”和“赋值”的复合操作。但它的行为对于可变和不可变对象有本质区别,这是另一个高频踩坑点。

  • 对不可变对象a += b完全等价于a = a + b。它会先计算a + b产生一个新对象,然后让a指向这个新对象。
x = 5 print(id(x)) # 假设地址是 140736040000000 x += 2 # 等价于 x = x + 2 print(x) # 7 print(id(x)) # 地址变了!是一个新的整数对象7。
  • 对可变对象a += b不一定等价于a = a + b。对于列表等类型,+=是原地操作(调用__iadd__方法),而+是创建一个新对象(调用__add__方法)。
list1 = [1, 2] list2 = list1 print(id(list1)) # 地址A # 使用 += (原地操作) list1 += [3, 4] # 等价于 list1.extend([3, 4]) print(list1) # [1, 2, 3, 4] print(id(list1)) # 地址A,没有改变!list1还是原来那个对象。 print(list2) # [1, 2, 3, 4]!list2也同步被修改了,因为它们指向同一个对象。 # 重置,使用 + (创建新对象) list1 = [1, 2] list2 = list1 print(id(list1)) # 地址B list1 = list1 + [3, 4] # 先计算新列表[1,2,3,4],再让list1指向它 print(list1) # [1, 2, 3, 4] print(id(list1)) # 地址C,改变了!list1现在指向一个新对象。 print(list2) # [1, 2]!list2仍然指向旧的列表对象,未被影响。

避坑指南:当你使用可变对象(尤其是列表),并且有多个变量引用它时,要格外小心使用+=。如果你本意是创建一个新列表而不影响原数据,应该使用a = a + b或者显式的拷贝(后面会讲)。

4. 多元赋值与序列解包

Python的赋值语句非常灵活,可以一次性给多个变量赋值。

4.1 链式赋值

将同一个对象赋值给多个变量。

a = b = c = [] # a, b, c 三个变量都指向同一个空列表对象 a.append(1) print(b) # [1] print(a is b and b is c) # True

注意:由于是共享引用,对a,b,c中任何一个进行修改(如果是可变对象),都会影响其他两个。这通常不是你想要的效果,使用时要谨慎。

4.2 平行赋值(序列解包)

这是Python中非常优雅和实用的特性,可以一次性将序列(或任何可迭代对象)中的元素分别赋值给多个变量。

基础形式

# 元组解包(括号常可省略) x, y, z = (1, 2, 3) print(x, y, z) # 1 2 3 # 列表解包 a, b, c = [‘hello‘, 3.14, True] print(a, b, c) # hello 3.14 True # 字符串解包(每个字符) c1, c2, c3 = ‘ABC‘ print(c1, c2, c3) # A B C

交换两个变量的值: 这是序列解包最经典的应用,无需临时变量。

a, b = 10, 20 print(f‘Before: a={a}, b={b}‘) # Before: a=10, b=20 a, b = b, a # 交换 print(f‘After: a={a}, b={b}‘) # After: a=20, b=10

右边b, a会先计算成一个元组(20, 10),然后解包赋值给左边的a, b

使用星号*处理剩余元素: 在Python 3中,你可以用*来捕获解包后剩余的所有元素,形成一个列表。

first, *middle, last = [1, 2, 3, 4, 5, 6] print(first) # 1 print(middle) # [2, 3, 4, 5] # 注意是列表 print(last) # 6 # 也可以只用在开头或中间 *front, last_two, last_one = range(10) print(front) # [0, 1, 2, 3, 4, 5, 6, 7] print(last_two, last_one) # 8 9 first, second, *rest = [‘a‘, ‘b‘] print(first, second, rest) # a b [] # rest是空列表

在循环中解包: 遍历包含子序列(如元组、列表)的序列时特别有用。

points = [(1, 2), (3, 4), (5, 6)] for x, y in points: # 每次循环,points中的一个元组被解包给x和y print(f‘({x}, {y})‘) # 遍历字典的.items() person = {‘name‘: ‘Alice‘, ‘age‘: 30, ‘city‘: ‘Beijing‘} for key, value in person.items(): print(f‘{key}: {value}‘)

注意事项:解包时,左边的变量个数必须与右边可迭代对象解包后的元素个数相等,除非使用了*来接收多余元素。否则会抛出ValueError

a, b = [1, 2, 3] # ValueError: too many values to unpack (expected 2) a, b, c = [1, 2] # ValueError: not enough values to unpack (expected 3, got 2)

5. 海象运算符:=(赋值表达式)

这是Python 3.8引入的新特性,被称为“海象运算符”(因为:=看起来像海象的眼睛和獠牙)。它允许在表达式内部进行赋值,并将赋值后的值作为整个表达式的值。

5.1 基本语法与使用场景

它的核心价值在于简化代码,尤其是在需要先计算一个值,然后立即在条件判断或另一个表达式中使用该值的场景。

传统写法

line = input(‘Enter something: ‘) while line != ‘quit‘: print(f‘You entered: {line}‘) line = input(‘Enter something: ‘)

这里line = input(...)重复写了两次。

使用海象运算符

while (line := input(‘Enter something: ‘)) != ‘quit‘: print(f‘You entered: {line}‘)

while条件判断中,我们同时完成了输入、赋值和比较三个操作,代码更紧凑。

5.2 更多实用例子

  1. 在列表推导式中复用计算值
# 读取文件,过滤掉空行,并转换为大写 lines = [line.upper() for line in open(‘file.txt‘) if (line := line.strip())] # 这里先执行 line := line.strip(),将去除首尾空格的结果赋值给line(并作为if的判断值) # 如果line不为空,则使用这个已经strip过的line进行 .upper() 操作

没有海象运算符的话,你需要写:

lines = [] for raw_line in open(‘file.txt‘): line = raw_line.strip() if line: lines.append(line.upper())
  1. 匹配正则表达式并直接使用匹配对象
import re pattern = re.compile(r‘\d+‘) text = ‘items: 123, 456, 789‘ if (match := pattern.search(text)): print(f‘Found: {match.group()}‘) # 直接使用match对象
  1. 简化“先计算,后判断”的模式
# 计算一个开销大的函数,结果用于判断和后续使用 data = get_expensive_data() if data is not None: process(data) # 可以写成 if (data := get_expensive_data()) is not None: process(data)

5.3 使用海象运算符的注意事项

  • 优先级:海象运算符的优先级非常低,仅高于yieldlambda。因此,在复杂表达式中使用它时,强烈建议加上括号,以明确赋值范围,避免歧义。上面所有的例子都加了括号。
  • 可读性:虽然它能简化代码,但过度使用或在不合适的复杂表达式中使用,会严重损害代码的可读性。切忌为了炫技而使用。一个简单的原则是:如果使用海象运算符让一行代码变得难以一眼看懂,那就拆成多行。
  • 作用域:海象运算符在表达式内部创建的变量,其作用域与它所在的作用域相同。在上面的while循环例子中,line变量在循环外部也是可访问的。

个人体会:海象运算符是一个“糖度”很高的语法糖。在清晰的、模式固定的场景下(如while循环读取输入、列表推导式中复用计算值),它能显著提升代码的简洁性。但在复杂的条件判断或嵌套表达式中,我建议还是使用传统的写法,毕竟代码是写给人看的,清晰永远是第一位的。

6. 对象的复制:浅拷贝与深拷贝

这是赋值话题中最关键、也最容易出错的部分。当你需要一份数据的“副本”而不是“别名”时,就必须使用拷贝。

6.1 为什么需要拷贝?

回到最初的例子:

original_list = [[1, 2], [3, 4]] copied_ref = original_list # 这只是创建了一个新引用 copied_ref.append([5, 6]) print(original_list) # [[1, 2], [3, 4], [5, 6]] 原列表被修改了!

我们只是想操作copied_ref,但original_list也被改变了。为了避免这种副作用,我们需要复制对象本身。

6.2 浅拷贝:只拷贝第一层

浅拷贝会创建一个新对象,其内容是原对象中顶层元素的引用。对于列表,就是创建一个新列表,但新列表里的元素仍然指向原列表元素所指向的那些对象。

实现浅拷贝的方法

  1. 使用切片操作new_list = old_list[:]
  2. 使用list()构造函数new_list = list(old_list)
  3. 使用copy模块的copy()方法import copy; new_list = copy.copy(old_list)
  4. 对于列表,使用.copy()方法new_list = old_list.copy()

示例与陷阱

import copy original = [[1, 2], [3, 4]] shallow_copied = original.copy() # 或 original[:] print(original is shallow_copied) # False,是两个不同的列表对象 print(original[0] is shallow_copied[0]) # True!内部的子列表是同一个对象 # 修改顶层结构,互不影响 shallow_copied.append([5, 6]) print(original) # [[1, 2], [3, 4]] print(shallow_copied) # [[1, 2], [3, 4], [5, 6]] # 修改共享的嵌套对象,互相影响! shallow_copied[0][0] = ‘X‘ print(original) # [[‘X‘, 2], [3, 4]] # 原列表也被改了! print(shallow_copied) # [[‘X‘, 2], [3, 4], [5, 6]]

浅拷贝只解决了“顶层”的独立性问题。如果对象内部包含其他可变对象(嵌套列表、字典等),这些内部对象仍然是共享的。这就是“浅”的含义。

6.3 深拷贝:递归拷贝所有层级

深拷贝会递归地拷贝原对象中的所有元素,创建一个完全独立的新对象。新对象和原对象不共享任何可变子对象。

实现方法:使用copy模块的deepcopy()函数。

import copy original = [[1, 2], [3, 4]] deep_copied = copy.deepcopy(original) print(original is deep_copied) # False print(original[0] is deep_copied[0]) # False!子列表也被复制了,是两个不同的对象 # 现在无论怎么修改深拷贝后的对象,都不会影响原对象 deep_copied[0][0] = ‘X‘ print(original) # [[1, 2], [3, 4]] # 保持不变 print(deep_copied) # [[‘X‘, 2], [3, 4]]

6.4 如何选择浅拷贝与深拷贝?

选择的标准取决于你的数据结构和使用场景。

特性浅拷贝 (copy.copy)深拷贝 (copy.deepcopy)
拷贝深度仅顶层对象递归所有嵌套对象
速度与内存快,占用内存少慢,占用内存多(递归复制所有)
独立性顶层独立,嵌套对象共享完全独立
适用场景对象内部元素均为不可变类型,或你明确知道且允许共享嵌套对象对象结构复杂,包含多层嵌套的可变对象,且你需要一份完全独立的副本

决策流程建议

  1. 如果你的对象只包含不可变类型(如int,str,tuple),浅拷贝和深拷贝效果一样,用浅拷贝(更快)。
  2. 如果你的对象包含可变子对象,但你希望共享它们(例如,多个配置字典共享同一个默认值字典),用浅拷贝。
  3. 如果你的对象包含可变子对象,且你需要一份完全隔离、互不影响的副本(例如,在函数中修改传入的列表参数而不想影响调用方),用深拷贝。
  4. 不确定时,或者数据结构非常复杂,为了安全起见,使用深拷贝。

避坑技巧:对于自定义的类实例,拷贝行为取决于类是否定义了__copy__()__deepcopy__()方法。如果没有定义,copydeepcopy会使用默认策略。如果你需要特殊的拷贝逻辑(比如忽略某些内部状态),可以在类中实现这两个方法。

7. 赋值语句在函数传参中的应用

在Python中,函数的参数传递既不是“传值”,也不是“传引用”,而是“传对象引用”。这实际上是赋值语句的一种应用:将实参(一个对象引用)赋值给形参(函数内部的局部变量名)。

7.1 参数传递的本质

当你调用函数func(a)时,发生的过程类似于在函数内部执行了parameter = a。因此,参数传递遵循我们之前讨论的所有赋值规则。

def modify_number(x): print(f‘Inside func, id of x: {id(x)}‘) x = x + 10 # 由于整数不可变,这里创建了新对象,x指向新对象 print(f‘After change, id of x: {id(x)}‘) return x num = 5 print(f‘Outside, id of num: {id(num)}‘) result = modify_number(num) print(f‘Result: {result}‘) print(f‘Original num: {num}‘) # num 仍然是 5,未改变 print(f‘Outside, id of num after call: {id(num)}‘) # id 未变

输出会显示,刚进入函数时,形参x和实参num指向同一个整数对象5。但当执行x = x + 10后,x指向了新对象15,而外部的num仍然指向原来的5

7.2 可变对象作为参数:可能产生副作用

当传入的参数是可变对象(如列表、字典)时,情况就不同了。因为函数内部的形参和外部实参指向同一个对象,在函数内部修改这个对象的内容,会直接影响外部的实参。

def append_to_list(some_list, item): some_list.append(item) # 原地修改了传入的列表对象 print(f‘Inside func: {some_list}‘) my_list = [1, 2, 3] print(f‘Before call: {my_list}‘) append_to_list(my_list, 4) print(f‘After call: {my_list}‘) # my_list 被改变了!

这是一种副作用。有时这是你期望的(比如一个用于修改数据结构的函数),但很多时候这是意外的Bug源头。

7.3 如何避免意外的副作用?

  1. 明确函数职责:在函数文档字符串中清晰说明函数是否会修改传入的参数。
  2. 在函数内部创建副本:如果不希望修改原数据,在函数内部先做拷贝。
    def safe_append(some_list, item): """返回添加了新元素的新列表,不修改原列表。""" new_list = some_list.copy() # 或 list(some_list), some_list[:] new_list.append(item) return new_list my_list = [1, 2, 3] new_list = safe_append(my_list, 4) print(my_list) # [1, 2, 3] 未改变 print(new_list) # [1, 2, 3, 4]
  3. 使用不可变类型作为参数:如果可能,设计函数时让参数接受不可变类型(如元组),从源头避免被修改。
    def process_data(data_tuple): # data_tuple 是元组,无法被修改,安全 total = sum(data_tuple) return total

经验之谈:我个人的习惯是,除非函数的名字或目的明确表示它会修改输入(如sort_list_in_place,update_config),否则默认函数应该保持“纯函数”特性,即不产生副作用,相同的输入永远得到相同的输出,且不修改任何外部状态。这会让代码更容易测试、理解和维护。

8. 常见问题与排查技巧实录

在实际开发中,由赋值和引用引发的问题往往隐蔽且令人困惑。下面是我总结的几个典型场景和排查思路。

8.1 问题:函数返回后,我的列表怎么变了?

场景:你写了一个函数处理列表,函数内部好像创建了新的列表,但函数调用后,传入的原始列表却被修改了。

def process(data): temp = data # 你以为这里创建了副本? temp.append(‘processed‘) return temp original = [1, 2, 3] result = process(original) print(‘Result:‘, result) # [1, 2, 3, ‘processed‘] print(‘Original:‘, original) # [1, 2, 3, ‘processed‘] # 啊?原数据怎么也被改了?

根因temp = data只是创建了一个新的引用,tempdata指向同一个列表对象。后续的append操作修改了这个共享对象。解决:在函数内部,如果需要操作副本,必须显式拷贝。

def process(data): temp = data.copy() # 或者 data[:], list(data) temp.append(‘processed‘) return temp

8.2 问题:使用*运算符初始化嵌套列表的陷阱

场景:你想快速创建一个二维列表(矩阵),但修改其中一个元素,整列都变了。

matrix = [[0] * 3] * 4 # 创建一个4行3列的“矩阵” print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]] matrix[0][0] = 1 print(matrix) # 期望:[[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]] # 实际:[[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]] # 所有行的第一列都变了!

根因[0] * 3创建了一个列表[0, 0, 0][[0]*3] * 4是将这个同一个列表对象重复了4次,生成了一个包含4个引用的列表,这4个引用都指向内存中同一个[0,0,0]列表。修改其中任何一个,其他“行”自然同步变化。解决:使用列表推导式来确保每一行都是独立创建的新列表。

# 正确做法 matrix = [[0 for _ in range(3)] for _ in range(4)] # 或者 matrix = [[0] * 3 for _ in range(4)] matrix[0][0] = 1 print(matrix) # [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

8.3 问题:默认参数是可变对象导致的诡异行为

这是Python中最著名的“坑”之一。

def add_item(item, my_list=[]): # 危险!默认参数是可变对象 my_list.append(item) return my_list print(add_item(‘a‘)) # [‘a‘] print(add_item(‘b‘)) # [‘a‘, ‘b‘] # 咦?怎么上次的结果还在? print(add_item(‘c‘)) # [‘a‘, ‘b‘, ‘c‘]

根因:函数的默认参数在函数定义时就被求值并绑定,而不是在每次调用时。因此,my_list=[]中的[]在定义函数时就创建了,并且在整个程序运行期间,所有未提供该参数的调用都共享这同一个列表对象。解决:永远不要使用可变对象作为函数参数的默认值。应该使用None作为默认值,然后在函数内部进行判断和初始化。

def add_item(item, my_list=None): if my_list is None: # 注意用 is None 判断 my_list = [] # 每次调用都创建一个新的空列表 my_list.append(item) return my_list print(add_item(‘a‘)) # [‘a‘] print(add_item(‘b‘)) # [‘b‘] # 符合预期

8.4 调试技巧:如何快速定位引用问题?

当怀疑是引用共享导致的数据错误时,可以按以下步骤排查:

  1. 打印id():在关键位置打印变量的id(),看它们是否指向同一个对象。
  2. 使用is判断:直接使用a is b判断两个变量是否为同一对象。
  3. 画内存图:对于复杂的数据结构,在纸上画一下变量名和对象的引用关系图,能非常直观地发现问题。
  4. 利用调试器:在IDE(如PyCharm, VSCode)中使用调试器,查看变量的内存地址和值的变化过程。

赋值语句是Python编程的原子操作,理解它背后的“引用”模型,是写出正确、高效Python代码的基石。从简单的a = b到复杂的深浅拷贝、函数传参,其核心逻辑一以贯之。希望这篇长文能帮你彻底理清这些概念,在未来的编码中少走弯路。记住,当你对一段涉及数据修改的代码行为不确定时,多问自己一句:“这几个变量,到底指向的是不是同一个对象?”

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询