Logo
Published on

2.8.变量与内存

Authors
  • avatar
    Name
    xiaobai
    Twitter

1.变量是什么?

1.1.变量的本质

在 Python 中,变量不是存储数据的容器,而是贴在对象上的标签。这是理解 Python 变量最重要的概念。

a = 10

这行代码的真实含义:

  1. 内存中创建一个整数对象 10创建一个名为 a 的变量(标签)
  2. 将标签 a 贴在对象 10 上(建立引用关系)
Python 的变量(标签模型):
变量 a ────→ [整数对象 10]
             (内存地址: 0x1000)
  • Python 中,变量是对象的引用,指向内存中的对象
img

2.变量的命名规则

2.1.硬性规则(必须遵守)

规则说明示例
字符限制只能包含字母、数字、下划线name_1, _value
开头字符不能以数字开头2name
大小写敏感大小写是不同的变量ageAgeAGE
不能是关键字不能使用 Python 保留字if, for, class
# 合法的变量名
name = "Alice"
user_age = 25
_country = "USA"
value2 = 100

# 非法的变量名
2name = "Bob"      # 错误!以数字开头
user-age = 30      # 错误!包含连字符
class = "Math"     # 错误!class 是关键字

2.2.风格约定(强烈推荐)

遵循 PEP 8 编码规范:

# 推荐:蛇形命名法(snake_case)
user_name = "Charlie"
total_count = 42
file_path = "/docs/report.pdf"

# 不推荐:驼峰命名法(用于类名)
userName = "Charlie"
TotalCount = 42

# 见名知意
email_address = "user@example.com"
max_retry_count = 3

# 不清晰
e = "user@example.com"
mrc = 3

2.3.特殊命名约定

# 单下划线开头:内部使用,不建议外部访问
_internal_var = "private"

# 双下划线开头:类的私有成员
__private_var = "really private"

# 双下划线前后:魔术方法/特殊方法
__init__, __str__, __len__

# 单下划线:临时变量或不重要的变量
for _ in range(5):
    print("Hello")
  • __init__:构造方法,用于初始化对象。
  • __str__:定义对象的字符串表示,通常用于print()输出。
  • __len__:定义对象的长度,支持len()函数。

这些被称为“魔术方法”或“特殊方法”,在类中有特殊用途。

3.变量的赋值

3.1.基本赋值

# 基本类型赋值
message = "Hello, World!"
pi = 3.14159
is_active = True
count = 0

3.2.多重赋值

# 同时为多个变量赋同一个值
a = b = c = 100
print(a, b, c)  # 100 100 100

# 同时为多个变量赋不同的值(元组解包)
name, age, city = "Alice", 30, "Beijing"
print(name)  # Alice
print(age)   # 30
print(city)  # Beijing

3.3.变量交换

Python 中交换变量非常优雅:

# Python 方式(推荐)
x, y = 10, 20
x, y = y, x
print(x, y)  # 20 10

# 传统方式(不推荐)
x, y = 10, 20
temp = x
x = y
y = temp
print(x, y)  # 20 10

3.4.链式赋值的陷阱

# 注意:共享同一个对象
a = b = [1, 2, 3]
a.append(4)
print(b)  # [1, 2, 3, 4] - b 也被修改了!

# 正确方式:分别赋值
a = [1, 2, 3]
b = [1, 2, 3]  # 创建新对象
a.append(4)
print(b)  # [1, 2, 3] - b 没有被修改

4.深入理解:变量与对象

4.1.一切皆对象

在 Python 中,一切都是对象,包括数字、字符串、函数、类等。

# 数字是对象
x = 42
print(type(x))  # <class 'int'>
print(isinstance(x, object))  # True

# 函数也是对象
def greet():
    return "Hello"

print(type(greet))  # <class 'function'>
print(isinstance(greet, object))  # True
  • type(obj):用于查看对象的类型。例如,type(123) 返回 <class 'int'>type("abc") 返回 <class 'str'>
  • isinstance(obj, class_or_tuple):判断一个对象是否是某个类型(或类型元组)的实例。例如,isinstance(123, int) 返回 Trueisinstance("abc", (int, str)) 返回 True

4.2.动态类型

变量本身没有类型,类型属于对象。同一个变量可以指向不同类型的对象。

var = 100
print(var, type(var))  # 100 <class 'int'>

var = "Now I'm a string"
print(var, type(var))  # Now I'm a string <class 'str'>

var = [1, 2, 3]
print(var, type(var))  # [1, 2, 3] <class 'list'>

var = {"key": "value"}
print(var, type(var))  # {'key': 'value'} <class 'dict'>

4.3.对象的身份:id() 和 is

# id() 返回对象的唯一标识(内存地址)
a = [1, 2, 3]
print(id(a))  # 例如:140245678901234

# is 检查是否是同一个对象
b = a
c = [1, 2, 3]

print(a is b)  # True - 指向同一个对象
print(a is c)  # False - 指向不同的对象
print(a == c)  # True - 内容相同

# 查看 id
print(id(a))  # 140245678901234
print(id(b))  # 140245678901234 (相同)
print(id(c))  # 140245678905678 (不同)

重要区别

  • is:比较对象的身份(内存地址是否相同)
  • ==:比较对象的值(内容是否相同)

5.内存指向关系

5.1.多个变量指向同一个对象

a = 100
b = a

内存图示:

变量 a ────┐
      [整数对象 100]
变量 b ────┘
(地址: 0x2000)

验证代码:

a = 100
b = a

print(a is b)           # True - 指向同一个对象
print(id(a) == id(b))   # True - 内存地址相同

# 修改 a 会怎样?
a = 200
print(a)  # 200
print(b)  # 100 - b 仍然指向原来的对象

# 内存变化
print(a is b)  # False - 现在指向不同对象

关键理解a = 200 不是修改了对象 100 的值,而是创建了新对象 200,然后让 a 指向这个新对象。

img

5.2.相同值的不同对象

a = [1, 2, 3]
b = [1, 2, 3]

内存图示:

变量 a ───→ [列表对象 [1,2,3]]
            (地址: 0x3000)

变量 b ───→ [列表对象 [1,2,3]]
            (地址: 0x4000)

验证代码:

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # True - 值相等
print(a is b)  # False - 不是同一个对象
print(id(a), id(b))  # 两个不同的地址

# 修改 a 不会影响 b
a.append(4)
print(a)  # [1, 2, 3, 4]
print(b)  # [1, 2, 3]
img

5.3.小整数缓存机制

Python 对整数和短字符串进行了缓存优化(驻留机制)。

# 整数缓存
a = 100
b = 100
print(a is b)  # True - 指向同一个缓存对象
# 字符串驻留
s1 = "hello"
s2 = "hello"
print(s1 is s2)  # True - 指向同一个对象

# 包含特殊字符的字符串可能不驻留
s3 = "hello"*10000
s4 = "hello"*10000
print(s3 is s4)  # 可能是 False

6.可变对象 vs 不可变对象

这是理解 Python 内存管理的最关键部分

6.1.不可变对象(Immutable)

不可变对象一旦创建,其值就不能被修改。

常见的不可变类型包括:

  • int(整数):一旦赋值,数值不可更改。
  • float(浮点数):数值不可更改。
  • str(字符串):内容不可更改,任何修改都会生成新字符串对象。
  • tuple(元组):元素不可更改。
  • frozenset(冻结集合):集合内容不可更改。
  • bytes(字节串):内容不可更改。

这些类型的对象在创建后,其内部数据不能被修改。如果对它们进行“修改”操作,实际上是创建了一个新的对象,原对象保持不变。

# 数字示例
x = 5
print(f"x 的地址: {id(x)}")

x = x + 1  # 不是修改 5,而是创建新对象 6
print(f"x 的新地址: {id(x)}")  # 地址改变了
# 字符串示例
s = "hello"
print(f"s 的地址: {id(s)}")

s = s + " world"  # 创建新字符串
print(f"s 的新地址: {id(s)}")  # 地址改变了
# 元组示例
t = (1, 2, 3)
print(f"t 的地址: {id(t)}")

# t[0] = 99  # 错误!元组不可修改

t = (1, 2, 3, 4)  # 创建新元组
print(f"t 的新地址: {id(t)}")  # 地址改变了

不可变对象的特点

  • 修改操作会创建新对象
  • 多个变量可以安全地共享同一个不可变对象
  • 可以作为字典的键

6.2.可变对象(Mutable)

可变对象的值可以在原地修改,不创建新对象。

  • list(列表):可以在原地添加、删除或修改元素。
  • dict(字典):可以动态添加、删除或修改键值对。
  • set(集合):可以添加或移除元素,集合内容可变。
  • bytearray(可变字节序列):可以修改其中的字节内容。
# 列表示例
lst = [1, 2, 3]
print(f"列表地址: {id(lst)}")

lst.append(4)  # 原地修改
print(f"append 后: {lst}")
print(f"列表地址: {id(lst)}")  # 地址不变

lst[0] = 99  # 原地修改元素
print(f"修改后: {lst}")
print(f"列表地址: {id(lst)}")  # 地址仍不变
# 字典示例
d = {"name": "Alice", "age": 25}
print(f"字典地址: {id(d)}")

d["city"] = "Beijing"  # 原地添加键值对
print(f"添加后: {d}")
print(f"字典地址: {id(d)}")  # 地址不变

可变对象的特点

  • 可以在原地修改
  • 多个变量共享可变对象时需要小心
  • 不能作为字典的键

6.3.可变对象的共享风险

当多个变量引用同一个可变对象时,对其中一个变量的修改会影响到所有引用该对象的变量。这种“共享”带来的副作用,容易导致程序出现难以察觉的 bug。

例如,列表、字典等可变对象在赋值时只是复制了引用(地址),并没有创建新对象。如果需要避免这种风险,应当使用 .copy() 方法或切片 [:] 来创建副本。

常见风险场景包括:

  • 作为函数参数传递可变对象,函数内部的修改会影响外部变量。
  • 多个变量指向同一个可变对象,任意一个变量的修改都会影响其他变量。
# 风险示例
list1 = [1, 2, 3]
list2 = list1  # list2 和 list1 指向同一个列表

list1.append(4)
print(list2)  # [1, 2, 3, 4] - list2 也被修改了!

# 安全方式:创建副本
list1 = [1, 2, 3]
list2 = list1.copy()  # 或 list2 = list1[:]

list1.append(4)
print(list1)  # [1, 2, 3, 4]
print(list2)  # [1, 2, 3] - list2 不受影响

6.4.不可变对象包含可变对象

有些不可变对象(如元组)可以包含可变对象(如列表、字典)。虽然元组本身是不可变的,但其内部的可变对象内容是可以被修改的。这种情况下,元组的“不可变性”只体现在其结构(元素的引用)不能更改,但元素所指向的可变对象内容可以发生变化。

示例

# 元组是不可变的,但可以包含可变对象
t = (1, 2, [3, 4])

# 不能修改元组本身
# t[0] = 99  # 错误!

# 但可以修改元组中的可变对象
t[2].append(5)
print(t)  # (1, 2, [3, 4, 5])

# 元组的 id 没变,但内容变了

7.以下内容不用看,Python全部学完后再看

8.实际应用场景

8.1.函数参数传递

Python 的参数传递是传递对象引用(有时称为"传对象引用")。

  • 传递可变对象:函数内的修改会影响外部
  • 传递不可变对象:函数内的修改不会影响外部
  • 函数内重新赋值:不会影响外部变量
def modify_list(lst):
    """修改列表(可变对象)"""
    lst.append(4)
    print(f"函数内 lst: {lst}, id: {id(lst)}")

def modify_number(num):
    """修改数字(不可变对象)"""
    num = num + 1
    print(f"函数内 num: {num}, id: {id(num)}")

def reassign_list(lst):
    """重新赋值列表"""
    lst = [7, 8, 9]  # 创建新对象
    print(f"函数内 lst: {lst}, id: {id(lst)}")

# 测试可变对象
my_list = [1, 2, 3]
print(f"调用前: {my_list}, id: {id(my_list)}")
modify_list(my_list)
print(f"调用后: {my_list}")  # [1, 2, 3, 4] - 被修改了!

# 测试不可变对象
my_num = 10
print(f"\n调用前: {my_num}, id: {id(my_num)}")
modify_number(my_num)
print(f"调用后: {my_num}")  # 10 - 没有被修改

# 测试重新赋值
print(f"\n调用前: {my_list}, id: {id(my_list)}")
reassign_list(my_list)
print(f"调用后: {my_list}")  # [1, 2, 3, 4] - 没有被修改

8.2.默认参数的陷阱

默认参数的陷阱主要出现在可变对象(如列表、字典等)作为函数默认参数时。 Python 只在函数定义时计算一次默认参数,这意味着如果默认参数是可变对象,后续对它的修改会影响到后续的函数调用。

# 危险:可变对象作为默认参数
def add_item(item, lst=[]):
    lst.append(item)
    return lst

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - 意外!
print(add_item(3))  # [1, 2, 3] - 意外!

# 正确方式
def add_item_safe(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

print(add_item_safe(1))  # [1]
print(add_item_safe(2))  # [2]
print(add_item_safe(3))  # [3]

上面例子中,add_itemlst=[] 只在函数定义时创建了一次。每次调用 add_item 时,如果没有传入 lst,都会用同一个列表对象。这会导致意外的累积效果。

正确做法:将默认参数设为 None,在函数内部判断并创建新对象,避免共享同一个可变对象。

这样可以保证每次调用函数时,默认参数都是一个全新的列表,不会互相影响。

8.3.浅拷贝 vs 深拷贝

浅拷贝(shallow copy)和深拷贝(deep copy)是 Python 中用于复制对象的两种方式,尤其在处理包含嵌套可变对象(如列表、字典等)的复合对象时非常重要。

  • 浅拷贝:只复制最外层对象本身,内部嵌套的对象依然是原对象的引用。也就是说,外层是新对象,但内层还是指向同一个内存地址。如果修改内层可变对象,原对象和拷贝对象都会受到影响。
  • 深拷贝:不仅复制最外层对象,还递归地复制所有嵌套的对象。这样,原对象和拷贝对象完全独立,互不影响。

常见的拷贝方式有:

  • 直接赋值(b = a):不会创建新对象,ab 指向同一个对象。
  • 浅拷贝(如 b = a.copy()b = copy.copy(a)b = a[:]):只复制外层。
  • 深拷贝(b = copy.deepcopy(a)):完全递归复制所有层级。
import copy

# 原始列表(包含嵌套列表)
original = [1, 2, [3, 4]]

# 1. 直接赋值(共享引用)
assigned = original
assigned[0] = 99
print(original)  # [99, 2, [3, 4]] - 被修改了

# 2. 浅拷贝(只拷贝第一层)
original = [1, 2, [3, 4]]
shallow = copy.copy(original)
# 或 shallow = original.copy()
# 或 shallow = original[:]

print(original is shallow)  # False - 外层是新对象
print(original[2] is shallow[2])  # True - 内层是同一个对象

shallow[0] = 100
print(original)  # [1, 2, [3, 4]] - 没有被影响

shallow[2].append(5)
print(original)  # [1, 2, [3, 4, 5]] - 被影响了!

# 3. 深拷贝(完全独立的副本)
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)

print(original[2] is deep[2])  # False - 完全独立

deep[2].append(5)
print(original)  # [1, 2, [3, 4]] - 没有被影响
print(deep)     # [1, 2, [3, 4, 5]]

拷贝方法对比

方法语法外层对象内层对象使用场景
赋值b = a共享共享需要多个引用同一对象
浅拷贝b = a.copy()独立共享只需要独立的第一层
深拷贝b = copy.deepcopy(a)独立独立需要完全独立的副本

8.4.循环引用和垃圾回收

循环引用是指对象之间互相引用,形成一个“环”,导致它们的引用计数都不为零,从而无法被简单的引用计数机制回收。例如,A 引用 B,B 又引用 A,即使外部变量都被删除,A 和 B 依然互相持有对方的引用。

Python 的垃圾回收机制(GC)除了引用计数外,还采用了“分代垃圾回收”算法,能够检测并回收这种循环引用的对象。常用的 gc 模块可以手动触发垃圾回收,也可以查看当前不可达但未被回收的对象。

注意事项

  • 带有 __del__ 方法的对象如果参与循环引用,可能不会被及时回收,因为解释器无法确定回收顺序,可能导致资源泄漏。
  • 一般情况下无需手动干预,除非处理大量对象或特殊资源管理需求。
import gc

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
    
    def __del__(self):
        print(f"Node {self.value} 被回收")

# 创建循环引用
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1  # 形成环

# 删除引用
del node1
del node2

# 手动触发垃圾回收
print("触发垃圾回收...")
gc.collect()  # Python 的垃圾回收器会处理循环引用

9.最佳实践

9.1.变量命名

良好的变量命名有助于代码的可读性和可维护性。建议遵循以下原则:

  • 见名知意:变量名应能准确反映其用途或含义,避免使用无意义的缩写。
  • 遵循命名规范:Python 推荐使用小写字母和下划线(snake_case)风格命名变量。
  • 避免与关键字或内置函数重名:如 list, str, id 等。
  • 适当使用前缀/后缀:如 is_has_num__list 等,提升语义清晰度。
  • 长度适中:变量名不宜过短或过长,能表达清楚含义即可。
# 好的命名
user_email = "user@example.com"
max_retry_count = 3
is_authenticated = True
total_price = 99.99

# 不好的命名
e = "user@example.com"
mrc = 3
flag = True
tp = 99.99

9.2.使用 is 和 == 的时机

在 Python 中,is== 虽然都可以用来进行比较,但它们的含义和适用场景不同:

  • is 用于判断两个变量是否引用自同一个对象(即对象的身份是否相同)。常用于和 NoneTrueFalse 等单例对象的比较,或需要判断两个变量是否为同一对象时。
  • == 用于判断两个对象的值是否相等(即内容是否相同)。如果对象实现了 __eq__ 方法,则会调用该方法进行值的比较。

最佳实践

  • 判断是否为 None,应使用 isis not,而不是 ==
  • 判断两个变量是否为同一对象,使用 is
  • 判断值是否相等,使用 ==
  • 对于布尔值判断,直接用变量本身即可,无需 == True== False
# 检查是否为 None(使用 is)
value = None
if value is None:  #  正确
    print("value 是 None")

if value == None:  #  不推荐
    print("value 是 None")

# 检查布尔值
is_active = True
if is_active:  #  简洁
    print("活跃")

if is_active == True:  #  冗余
    print("活跃")

# 比较对象身份(使用 is)
a = [1, 2, 3]
b = a
if a is b:  #  检查是否是同一个对象
    print("同一个对象")

# 比较对象值(使用 ==)
c = [1, 2, 3]
if a == c:  #  检查值是否相等
    print("值相等")

9.3.避免可变对象的陷阱

在 Python 中,变量可以引用可变对象(如列表、字典、集合等)或不可变对象(如整数、字符串、元组等)。可变对象的一个常见陷阱是:多个变量可能引用同一个可变对象,导致对其中一个变量的修改会影响到其他变量。

9.3.1.可变默认参数的陷阱

函数定义时如果使用可变对象作为默认参数,这个对象会在函数定义时被创建,并在后续所有调用中被共享。例如:

# 危险:可变默认参数
def append_to(element, target=[]):
    target.append(element)
    return target

# 安全:使用 None
def append_to_safe(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target
print(append_to(1))
print(append_to(2))
print(append_to_safe(1))
print(append_to_safe(2))

9.3.2.共享可变对象

在 Python 中,使用 [[0] * cols] * rows 创建二维列表时,实际上是将同一个子列表的引用复制多次。这样导致所有的行实际上指向同一个列表对象,对其中一行的修改会影响到所有行。这是因为可变对象(如列表)在复制引用时不会创建新的对象。

# 危险:共享可变对象
def create_matrix(rows, cols):
    return [[0] * cols] * rows  # 所有行共享同一个列表

# 安全:创建独立对象
def create_matrix_safe(rows, cols):
    return [[0] * cols for _ in range(rows)]

matrix = create_matrix(3, 3)
matrix[0][0] = 1
print(matrix)
matrix_safe = create_matrix_safe(3, 3)
matrix_safe[0][0] = 1
print(matrix_safe)

9.4.合理使用拷贝

在 Python 中,拷贝对象时需要根据实际需求选择浅拷贝(shallow copy)还是深拷贝(deep copy)。

  • 浅拷贝:只复制最外层对象,内部嵌套的可变对象依然是原来的引用。常用方法有 list.copy()、切片 [:]copy.copy() 等。
  • 深拷贝:递归复制所有嵌套对象,生成完全独立的新对象。使用 copy.deepcopy() 实现。
# 简单列表:使用浅拷贝
simple_list = [1, 2, 3, 4]
copy1 = simple_list.copy()
copy2 = simple_list[:]
copy3 = list(simple_list)

# 嵌套结构:使用深拷贝
import copy
nested_list = [1, [2, 3], [4, [5, 6]]]
deep_copy = copy.deepcopy(nested_list)

9.5.调试内存问题

import sys

def analyze_object(obj, name):
    """分析对象的内存信息"""
    print(f"\n{name} 的分析:")
    print(f"  类型: {type(obj)}")
    print(f"  值: {obj}")
    print(f"  ID: {id(obj)}")
    print(f"  大小: {sys.getsizeof(obj)} bytes")
    print(f"  引用计数: {sys.getrefcount(obj)}")

# 使用示例
my_list = [1, 2, 3]
analyze_object(my_list, "my_list")

another_ref = my_list
analyze_object(my_list, "my_list (多了一个引用)")
  • getsizeof:用于获取对象占用的内存字节数(不包括其引用的子对象)。
  • getrefcount:用于获取对象当前的引用计数(即有多少个引用指向该对象)。

10.总结

10.1.核心要点

  1. 变量是标签,不是容器
    • 变量存储的是对象的引用,不是对象本身
    • 理解这点是掌握 Python 的关键
  2. 对象的身份和值
    • 使用 id() 查看对象的身份(内存地址)
    • 使用 is 比较身份,使用 == 比较值
  3. 可变 vs 不可变
    • 不可变对象:修改时创建新对象(int, str, tuple)
    • 可变对象:可以原地修改(list, dict, set)
  4. 参数传递机制
    • Python 传递对象引用
    • 修改可变对象会影响外部
    • 重新赋值不会影响外部
  5. 拷贝的选择
    • 赋值:共享引用
    • 浅拷贝:独立外层,共享内层
    • 深拷贝:完全独立

10.2.避免常见错误

  • 可变对象作为默认参数
  • 忽略浅拷贝和深拷贝的区别
  • 使用 == 比较 None
  • 不理解函数参数的引用传递
  • 在循环中创建共享的可变对象
img