【Python】列表的创建:[[] for _ in range(2)] 与 [[]] * 2有什么区别?
[[] for _ in range(2)] 与 [[]] * 2有什么区别
目录
- 写在前面
- 一、`[[]] * 2 `与`[[] for _ in range(2)] `的区别与联系
- **1. 示例代码**
- **2. 核心区别**
- **3. 验证实验**
- **(1) 初始状态**
- **(2) 修改子列表**
- **4. 内存结构图解**
- **(1) `mean_values_1 = [[]] * 2`**
- **(2) `mean_values_2 = [[] for _ in range(2)]`**
- **5. 实际场景分析**
- **错误示例:使用乘法运算符**
- **正确示例:使用列表推导式**
- **6. 总结**
- 二、 Python 列表(List)的创建方式总结
- Python 中列表(`list`)的创建方式灵活多样,以下是常见方法及其区别:
- **1. 直接使用方括号 `[]` 创建**
- **2. 使用 `list()` 构造函数**
- **`3. 列表推导式(List Comprehension)`**
- **`4. 使用 `*` 运算符重复元素`**
- **5. 通过循环动态构建**
- **6. 使用生成器表达式**
- **7. 解包操作(Python 3.5+)**
- **8. 使用 `copy` 模块(深拷贝)**
- **总结:各方法对比**
- **注意事项**
- 三、 Python 列表创建过程中的深复制与浅复制总结
- **1. 浅复制(Shallow Copy)**
- **(1) 使用 `*` 运算符**
- **(2) 列表推导式(引用现有对象)**
- **(3) `list()` 构造函数或切片**
- **(4) `copy.copy()`**
- **2. 深复制(Deep Copy)**
- **(1) `copy.deepcopy()`**
- **3. 无复制的场景**
- **(1) 直接赋值**
- **(2) 列表推导式(新建对象)**
- **4. 总结对比**
- **5. 常见陷阱与解决方案**
- **陷阱1:通过 `*` 初始化嵌套列表**
- **陷阱2:浅复制后意外修改嵌套对象**
- **6. 最佳实践**
写在前面
小编最近在写代码的时候遇到了一个问题,具体展示如下:
import numpy as npif __name__ == '__main__':mean_value = [[]] * 2data1 = np.random.randn(3, 4)data2 = np.random.randn(3, 4)data3 = [data1, data2]for i, data in enumerate(data3):value = np.mean(data, axis=1, keepdims=True)mean_value[i].append(value)mean_value = np.array(mean_value)print(mean_value.shape)print(len(mean_value[0]))
输出结果如下:
(2, 2, 3, 1)
2Process finished with exit code 0
Why?????
我想要的效果是mean_value存储两个数据的平均值,理论上来说shape应该是(2,
1
,3,1),但是这里为什么是(2,2
,3,1)??? 很不理解,查看代码逻辑没有问题,觉得应该是列表的创建出现了问题,于是小编换了一种列表的创建方式:
mean_value = [[] for _ in range(2)]
输出:(2, 1, 3, 1)
问题成功解决了!那么这两种创建列表的方式有什么不同呢?于是小编做了如下总结!
一、[[]] * 2
与[[] for _ in range(2)]
的区别与联系
在 Python 中,mean_values = [[]] * 2
和 mean_values = [[] for _ in range(2)]
都可以创建一个包含两个空列表的列表,但它们在 内存结构
和行为上有本质区别。
1. 示例代码
# 方法1:使用乘法运算符
mean_values_1 = [[]] * 2# 方法2:使用列表推导式
mean_values_2 = [[] for _ in range(2)]
2. 核心区别
特性 | mean_values_1 = [[]] * 2 | mean_values_2 = [[] for _ in range(2)] |
---|---|---|
内存分配 | 两个子列表是同一个对象的引用 | 每个子列表是独立的对象 |
修改子列表 | 修改一个子列表会影响另一个 | 修改一个子列表不影响另一个 |
适用场景 | 需要共享子列表的引用(极少使用) | 需要独立子列表(绝大多数场景) |
3. 验证实验
(1) 初始状态
print("初始状态:")
print("mean_values_1:", mean_values_1) # [[], []]
print("mean_values_2:", mean_values_2) # [[], []]
两个列表在初始状态下看起来相同。
(2) 修改子列表
# 向 mean_values_1 的第一个子列表添加元素
mean_values_1[0].append(1)# 向 mean_values_2 的第一个子列表添加元素
mean_values_2[0].append(1)print("修改后:")
print("mean_values_1:", mean_values_1) # [[1], [1]]
print("mean_values_2:", mean_values_2) # [[1], []]
- 关键区别:
mean_values_1
的两个子列表是同一个对象的引用,修改其中一个会影响另一个。mean_values_2
的两个子列表是独立的,修改互不影响。
4. 内存结构图解
(1) mean_values_1 = [[]] * 2
内存地址:
mean_values_1 --> [子列表A, 子列表A]
子列表A --> []
- 两个子列表指向同一内存地址。
(2) mean_values_2 = [[] for _ in range(2)]
内存地址:
mean_values_2 --> [子列表B, 子列表C]
子列表B --> []
子列表C --> []
- 每个子列表是独立的对象。
5. 实际场景分析
错误示例:使用乘法运算符
# 初始化一个 2x2 的零矩阵(错误做法)
matrix = [[0] * 2] * 2
matrix[0][0] = 1 # 修改一个元素会影响整列
print(matrix) # [[1, 0], [1, 0]]
- 问题:所有子列表是同一对象的引用,导致修改整列数据。
正确示例:使用列表推导式
# 初始化一个 2x2 的零矩阵(正确做法)
matrix = [[0 for _ in range(2)] for _ in range(2)]
matrix[0][0] = 1
print(matrix) # [[1, 0], [0, 0]]
- 解决:每个子列表独立,修改互不影响。
6. 总结
维度 | [[]] * 2 | [[] for _ in range(2)] |
---|---|---|
内存效率 | 高(共享子列表) | 低(独立子列表) |
数据安全性 | 低(可能引发意外修改) | 高(子列表独立) |
典型用途 | 需要共享引用的特殊场景 | 99% 的实际场景(如多维数组初始化) |
使用列表推导式: ([[] for _ in range(n)]
) 创建包含可变对象的嵌套列表,避免因共享引用导致的数据污染。
*
运算符重复元素: 其实就是对某个元素/列表的复制,本质上来说还是同一个元素/列表,属于欧浅复制,所以是共享内存的。
二、 Python 列表(List)的创建方式总结
Python 中列表(list
)的创建方式灵活多样,以下是常见方法及其区别:
1. 直接使用方括号 []
创建
a = [1, 2, 3] # 显式定义元素
b = [] # 创建空列表
- 特点:
- 最直观的方式,适合已知元素的情况。
- 支持混合数据类型(如
[1, "a", True]
)。
- 适用场景:静态初始化、简单列表。
2. 使用 list()
构造函数
a = list() # 空列表
b = list((1, 2, 3)) # 从元组转换
c = list("hello") # 从字符串转换 → ['h', 'e', 'l', 'l', 'o']
d = list(range(5)) # 从 range 对象转换 → [0, 1, 2, 3, 4]
- 特点:
- 可将任何可迭代对象(字符串、元组、生成器等)转换为列表。
- 若参数是字典,默认提取键(
list(dict)
→ 键的列表)。
- 适用场景:动态转换其他可迭代对象为列表。
3. 列表推导式(List Comprehension)
a = [x for x in range(5)] # [0, 1, 2, 3, 4]
b = [x**2 for x in range(5) if x%2==0] # 带条件筛选 → [0, 4, 16]
- 特点:
- 语法简洁,支持条件筛选和复杂表达式。
- 性能通常优于显式循环(底层优化)。
- 适用场景:动态生成复杂列表(如过滤、映射操作)。
4. 使用
* 运算符重复元素
a = [0] * 5 # [0, 0, 0, 0, 0]
b = [[0]] * 3 # [[0], [0], [0]](注意元素引用问题)
- 特点:
- 快速生成重复元素的列表。
- 注意:若元素是可变对象(如子列表),所有元素指向同一内存地址。
- 示例陷阱:
c = [[0]] * 3 c[0][0] = 1 # [[1], [1], [1]](所有子列表被修改)
- 适用场景:初始化不可变元素的重复列表(如数值、字符串)。
5. 通过循环动态构建
a = []
for i in range(5):a.append(i) # [0, 1, 2, 3, 4]
- 特点:
- 灵活性强,适合动态添加元素的场景。
- 性能略低于列表推导式(每次循环调用
append()
方法)。
- 适用场景:元素生成逻辑复杂,无法用推导式简化时。
6. 使用生成器表达式
a = list(x**2 for x in range(5)) # [0, 1, 4, 9, 16]
- 特点:
- 生成器表达式(
()
)惰性求值,节省内存。 - 通过
list()
转换为列表。
- 生成器表达式(
- 适用场景:处理大数据量时减少内存占用。
7. 解包操作(Python 3.5+)
a = [*range(5)] # [0, 1, 2, 3, 4]
b = [*"hello", 10] # ['h', 'e', 'l', 'l', 'o', 10]
- 特点:
- 使用
*
解包可迭代对象并合并到列表中。 - 类似
list()
,但更灵活(可与其他元素混合)。
- 使用
- 适用场景:合并多个可迭代对象或添加额外元素。
8. 使用 copy
模块(深拷贝)
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a) # 深拷贝,避免引用问题
- 特点:
- 深拷贝(
deepcopy
)完全复制嵌套结构,与原列表独立。 - 浅拷贝(
a.copy()
或a[:]
)仅复制外层,嵌套对象仍共享引用。
- 深拷贝(
- 适用场景:需要完全独立的嵌套列表。
总结:各方法对比
方法 | 语法简洁性 | 性能 | 内存效率 | 适用场景 |
---|---|---|---|---|
直接方括号 [] | ★★★★★ | 高 | 高 | 静态初始化已知元素 |
list() 构造函数 | ★★★★☆ | 中 | 中 | 转换其他可迭代对象为列表 |
列表推导式 | ★★★★★ | 高 | 高 | 动态生成复杂列表 |
* 运算符重复 | ★★★★☆ | 高 | 高 | 初始化不可变元素的重复列表 |
循环动态构建 | ★★★☆☆ | 低 | 中 | 复杂逻辑的动态生成 |
生成器表达式 + list() | ★★★★☆ | 中 | 高 | 大数据量的惰性转换 |
解包操作 | ★★★★☆ | 高 | 高 | 合并可迭代对象或混合元素 |
深拷贝 | ★★☆☆☆ | 低 | 低 | 嵌套结构的完全独立复制 |
注意事项
- 可变对象的引用问题:
使用*
运算符或浅拷贝时,若元素是可变对象(如子列表),修改一个元素会影响其他引用。 - 性能优化:
列表推导式通常比显式循环快,优先用于简单逻辑。 - 内存管理:
处理大数据时,生成器表达式(惰性求值)比列表推导式更节省内存。
三、 Python 列表创建过程中的深复制与浅复制总结
在 Python 中,列表的创建和复制行为分为 浅复制(Shallow Copy) 和 深复制(Deep Copy)。它们的核心区别在于对嵌套对象(如子列表、字典等)的处理方式。以下是不同列表创建方式的分类及详细说明:
1. 浅复制(Shallow Copy)
浅复制仅复制外层对象的引用,不递归复制嵌套对象。如果原对象包含可变元素(如子列表),修改这些元素会影响所有引用它们的对象。
(1) 使用 *
运算符
a = [[1, 2]] * 3 # 三个子列表指向同一个对象
a[0].append(3)
print(a) # [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
- 特点:所有子列表共享同一内存地址。
- 陷阱:修改任意子列表会影响所有其他子列表。
(2) 列表推导式(引用现有对象)
original = [[1, 2]]
b = [original[0] for _ in range(3)] # 三个子列表引用同一个对象
b[0].append(3)
print(b) # [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
- 特点:显式引用现有对象时,仍属于浅复制。
(3) list()
构造函数或切片
original = [[1, 2]]
c = list(original) # 浅复制外层列表
d = original[:] # 浅复制(等同于切片)
c[0].append(3)
print(original) # [[1, 2, 3]]
- 特点:外层列表是新的,但嵌套对象仍共享引用。
(4) copy.copy()
import copy
original = [[1, 2]]
e = copy.copy(original)
e[0].append(3)
print(original) # [[1, 2, 3]]
- 特点:与切片和
list()
行为一致,仅浅复制。
2. 深复制(Deep Copy)
深复制会递归复制所有嵌套对象,完全独立于原对象。修改深复制后的对象不会影响原对象。
(1) copy.deepcopy()
import copy
original = [[1, 2]]
f = copy.deepcopy(original)
f[0].append(3)
print(original) # [[1, 2]](原对象不受影响)
- 特点:完全独立的副本,适用于多层嵌套结构。
3. 无复制的场景
(1) 直接赋值
a = [1, 2]
b = a # 无复制,b 和 a 指向同一对象
b.append(3)
print(a) # [1, 2, 3]
- 特点:变量名是引用,不涉及任何复制操作。
(2) 列表推导式(新建对象)
c = [[] for _ in range(3)] # 每个子列表是独立对象
c[0].append(1)
print(c) # [[1], [], []]
- 特点:通过新建对象避免共享引用,但若元素本身是复杂对象且未深拷贝,仍可能浅复制。
4. 总结对比
方法 | 复制类型 | 嵌套对象独立性 | 适用场景 |
---|---|---|---|
* 运算符 | 浅复制 | ❌(共享引用) | 初始化不可变元素的重复列表 |
列表推导式(引用现有对象) | 浅复制 | ❌(共享引用) | 显式引用现有对象时 |
list() 或切片 | 浅复制 | ❌(共享引用) | 需要外层列表独立但嵌套共享时 |
copy.copy() | 浅复制 | ❌(共享引用) | 与切片行为一致 |
copy.deepcopy() | 深复制 | ✅(完全独立) | 需要完全独立的嵌套结构 |
列表推导式(新建对象) | 无复制 | ✅(独立对象) | 动态创建独立子列表(非复制场景) |
5. 常见陷阱与解决方案
陷阱1:通过 *
初始化嵌套列表
matrix = [[0] * 3] * 3 # 所有行共享同一列表
matrix[0][0] = 1 # 所有行的第一列被修改
- 解决方案:使用列表推导式创建独立子列表:
matrix = [[0 for _ in range(3)] for _ in range(3)]
陷阱2:浅复制后意外修改嵌套对象
original = [[1, 2]]
shallow_copy = original.copy()
shallow_copy[0].append(3) # 原对象被修改
- 解决方案:深复制嵌套对象:
import copy deep_copy = copy.deepcopy(original)
6. 最佳实践
- 默认使用列表推导式:
当需要独立子列表时,优先使用[[] for _ in range(n)]
,避免共享引用。 - 明确深复制需求:
对多层嵌套结构(如列表的列表、字典的列表等),使用copy.deepcopy()
。 - 警惕隐式共享引用:
使用*
运算符或浅复制时,确保元素是不可变对象(如整数、字符串)。