查看: 176|回复: 3

Python二维列表创建陷阱、深拷贝与矩阵运算实战

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
一维列表用熟之后,实际项目里真正难缠的往往是嵌套结构:电子表格是行×列,游戏棋盘是二维网格,图像像素也按通道×行×列排布。这类数据在 Python 中的基本载体就是二维列表——外层列表的每个元素又是一个列表。下面从创建方式讲起,重点拆解引用共享这个高频坑,再延伸到матриrix 运算、棋盘生成与表格处理等可直接复用的代码。

一、三种创建方式与它们的代价

小型固定数据直接写字面量最直观,索引规则是 matrix[行][列],负索引同样可用:
  1. matrix = [
  2.     [1, 2, 3],
  3.     [4, 5, 6],
  4.     [7, 8, 9],
  5. ]
  6. print(matrix[0])      # [1, 2, 3]
  7. print(matrix[1][2])   # 6
  8. print(len(matrix))    # 3 行
  9. print(len(matrix[0])) # 3 列
复制代码

需要动态生成规则矩阵时,用嵌套推导式,外层控制行、内层控制列,顺序不能颠倒:
  1. zeros = [[0 for _ in range(4)] for _ in range(3)]
  2. table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
  3. n = 4
  4. identity = [[1 if i == j else 0 for j in range(n)] for i in range(n)]
复制代码

第三种是乘法写法 [[0] * 4] * 3,看起来很简洁,实际上是 Python 最典型的陷阱之一。

二、乘法创建为什么会“一行改、行行变”

[0] * 4 会生成一个内层列表对象,外层再乘 3,只是把这个对象的引用复制了三份,三行在内存中指向同一块数据。于是修改任意一行,其余行同步变化:
  1. wrong = [[0] * 4] * 3
  2. wrong[0][0] = 999
  3. print(wrong)
  4. # [[999, 0, 0, 0], [999, 0, 0, 0], [999, 0, 0, 0]]
  5. print(wrong[0] is wrong[1])   # True
  6. print(id(wrong[0]), id(wrong[1]))  # 两个 id 完全相同
  7. correct = [[0] * 4 for _ in range(3)]
  8. correct[0][0] = 999
  9. print(correct)
  10. # [[999, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
  11. print(correct[0] is correct[1])   # False
复制代码

结论很明确:推导式每次循环都会执行一次内层 [0] * 4,得到独立的新列表;乘法只做引用复制。凡是嵌套列表的初始化,一律用推导式,不要用外层乘法。这个规则在三维及以上同样成立。

二维列表并不要求每行等长,锯齿结构在表示层级数据时反而更自然,例如部门与员工的从属关系:
  1. jagged = [
  2.     [1, 2, 3],
  3.     [4, 5],
  4.     [6, 7, 8, 9],
  5. ]
  6. for i, row in enumerate(jagged):
  7.     print(f'第{i}行有{len(row)}列: {row}')
复制代码

三、访问、遍历与常用行列操作

取单列、对角线都靠推导式完成,取子矩阵则可以先按行切片,再对每行做列切片:
  1. matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
  2. col_0 = [row[0] for row in matrix]                  # [1, 5, 9]
  3. main_diag = [matrix[i][i] for i in range(len(matrix))]  # [1, 6, 11]
  4. anti_diag = [matrix[i][len(matrix) - 1 - i] for i in range(len(matrix))]
  5. def submatrix(matrix, row_start, row_end, col_start, col_end):
  6.     return [row[col_start:col_end] for row in matrix[row_start:row_end]]
  7. print(submatrix(matrix, 0, 2, 1, 3))  # [[2, 3], [6, 7]]
复制代码

遍历推荐 enumerate 双重循环,既能拿到行列下标也能拿到值;如果只需要按顺序处理所有元素,可以用单层推导式展平:
  1. for i, row in enumerate(matrix):
  2.     for j, value in enumerate(row):
  3.         print(f'matrix[{i}][{j}] = {value}')
  4. flat = [cell for row in matrix for cell in row]
复制代码

修改方面,单个元素和整行都能直接赋值,整列必须循环,否则会替换掉整行对象:
  1. matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
  2. matrix[1][2] = 99          # 改单个元素
  3. matrix[0] = [10, 20, 30]   # 换整行
  4. for row in matrix:         # 改整列
  5.     row[1] = 0
  6. matrix.append([7, 8, 9])   # 加一行
  7. for i, row in enumerate(matrix):
  8.     row.append((i + 1) * 10)   # 每行尾部追加,等效加一列
  9. del matrix[1]              # 删一行
  10. for row in matrix:
  11.     del row[-1]            # 删一列
  12. matrix.insert(1, [100, 200, 300])
复制代码

增删列的本质是逐行操作,行数越多开销越大,批量场景下考虑整体重建而非原地修改。

四、矩阵转置、加法与乘法

转置用推导式可以写得很直白,新建 cols×rows 的结果矩阵,按下标搬运元素;更简洁的写法是借助 zip 解包:
  1. def transpose(matrix):
  2.     if not matrix:
  3.         return []
  4.     rows, cols = len(matrix), len(matrix[0])
  5.     return [[matrix[i][j] for i in range(rows)] for j in range(cols)]
  6. transposed2 = [list(col) for col in zip(*matrix)]
复制代码

加法和数乘都是逐元素运算,直接双层推导式即可:
  1. def matrix_add(a, b):
  2.     rows, cols = len(a), len(a[0])
  3.     return [[a[i][j] + b[i][j] for j in range(cols)] for i in range(rows)]
  4. def matrix_scalar_mul(matrix, scalar):
  5.     return [[cell * scalar for cell in row] for row in matrix]
复制代码

乘法必须先校验维度,左矩阵的列数要等于右矩阵的行数,结果规模是 rows_a×cols_b,内层用 sum 完成点积:
  1. def matrix_multiply(a, b):
  2.     if not a or not b:
  3.         return []
  4.     if len(a[0]) != len(b):
  5.         raise ValueError('矩阵尺寸不匹配')
  6.     rows_a, cols_a = len(a), len(a[0])
  7.     cols_b = len(b[0])
  8.     result = [[0] * cols_b for _ in range(rows_a)]
  9.     for i in range(rows_a):
  10.         for j in range(cols_b):
  11.             result[i][j] = sum(a[i][k] * b[k][j] for k in range(cols_a))
  12.     return result
  13. a = [[1, 2, 3], [4, 5, 6]]      # 2×3
  14. b = [[7, 8], [9, 10], [11, 12]] # 3×2
  15. # 结果 2×2:[[58, 64], [139, 154]]
  16. # 校验:1*7 + 2*9 + 3*11 = 58
复制代码

三重循环的时间复杂度是 O(n³),纯列表只适合教学或小规模数据;真实数值计算应交给 NumPy 之类的库。

五、棋盘类应用:井字棋与扫雷

井字棋把棋盘封装成类,落子前判断格子是否为空,胜负检查覆盖三行、三列和两条对角线,最后用生成器判断平局:
  1. class TicTacToe:
  2.     def __init__(self):
  3.         self.board = [[' ' for _ in range(3)] for _ in range(3)]
  4.         self.current_player = 'X'
  5.     def make_move(self, row, col):
  6.         if self.board[row][col] != ' ':
  7.             return False
  8.         self.board[row][col] = self.current_player
  9.         self.current_player = 'O' if self.current_player == 'X' else 'X'
  10.         return True
  11.     def check_winner(self):
  12.         b = self.board
  13.         for row in b:
  14.             if row[0] == row[1] == row[2] != ' ':
  15.                 return row[0]
  16.         for col in range(3):
  17.             if b[0][col] == b[1][col] == b[2][col] != ' ':
  18.                 return b[0][col]
  19.         if b[0][0] == b[1][1] == b[2][2] != ' ':
  20.             return b[0][0]
  21.         if b[0][2] == b[1][1] == b[2][0] != ' ':
  22.             return b[0][2]
  23.         if all(cell != ' ' for row in b for cell in row):
  24.             return '平局'
  25.         return None
  26.     def get_empty_cells(self):
  27.         return [(i, j) for i in range(3) for j in range(3)
  28.                 if self.board[i][j] == ' ']
复制代码

扫雷棋盘生成分两步:用 set 记录已放置的雷坐标避免重复,再把每个雷周围的 8 个方向格子数值加一,边界用范围判断过滤:
  1. import random
  2. def create_minesweeper(rows, cols, mines):
  3.     board = [[0 for _ in range(cols)] for _ in range(rows)]
  4.     mine_positions = set()
  5.     while len(mine_positions) < mines:
  6.         r = random.randint(0, rows - 1)
  7.         c = random.randint(0, cols - 1)
  8.         mine_positions.add((r, c))
  9.         board[r][c] = -1
  10.     directions = [(-1,-1), (-1,0), (-1,1), (0,-1),
  11.                   (0,1), (1,-1), (1,0), (1,1)]
  12.     for r, c in mine_positions:
  13.         for dr, dc in directions:
  14.             nr, nc = r + dr, c + dc
  15.             if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != -1:
  16.                 board[nr][nc] += 1
  17.     return board
复制代码

注意这里 board[r][c] 与周围计数必须在同一个棋盘对象上完成,如果误用乘法创建,所有行共享引用,数字会整体串位。

六、把二维列表当轻量表格用

CSV 文本按行切分、再按分隔符切列,就能得到二维列表;排序时保留表头,对表体用 lambda 指定列:
  1. def parse_csv_to_matrix(text, delimiter=','):
  2.     lines = text.strip().split('\n')
  3.     return [line.split(delimiter) for line in lines if line.strip()]
  4. def sort_by_column(data, col_index, numeric=False):
  5.     headers = data[0]
  6.     body = data[1:]
  7.     if numeric:
  8.         body.sort(key=lambda row: float(row[col_index]))
  9.     else:
  10.         body.sort(key=lambda row: row[col_index])
  11.     return [headers] + body
复制代码

透视表的思路是先用集合收集行列维度并排序,建立值到下标的映射字典,再用字典按 (行值, 列值) 分组聚合,最后回填到一个预先推导出的零矩阵里:
  1. def pivot_table(data, row_field, col_field, value_field, agg_func=sum):
  2.     row_values = sorted(set(d[row_field] for d in data))
  3.     col_values = sorted(set(d[col_field] for d in data))
  4.     pivot = [[0 for _ in range(len(col_values))] for _ in range(len(row_values))]
  5.     row_idx = {v: i for i, v in enumerate(row_values)}
  6.     col_idx = {v: i for i, v in enumerate(col_values)}
  7.     groups = {}
  8.     for record in data:
  9.         key = (record[row_field], record[col_field])
  10.         groups.setdefault(key, []).append(record[value_field])
  11.     for (r, c), values in groups.items():
  12.         pivot[row_idx[r]][col_idx[c]] = agg_func(values)
  13.     return pivot, row_values, col_values
复制代码

七、三维列表与拷贝层级问题

三维列表就是多个二维平面的堆叠,索引顺序为 [平面][行][列]。创建时必须用三层推导式,任何一层出现乘法都会造成跨层引用共享:
  1. depth, rows, cols = 2, 3, 4
  2. cube = [[[0 for _ in range(cols)] for _ in range(rows)] for _ in range(depth)]
  3. cube[0][1][2] = 99
  4. # 只有平面0的第1行第2列变为99,其他位置保持0
  5. # 错误写法:[[[0]*3]*4]*2 平面间、行间都会共享引用
复制代码

拷贝问题同样按层级区分。切片、copy()、list()、copy.copy() 都只复制外层,内层对象仍是共享的;要彻底隔离必须用 copy.deepcopy,它对二维以上的结构才安全:
  1. import copy
  2. original = [[1, 2], [3, 4]]
  3. shallow = original[:]
  4. shallow.append([5, 6])   # 改外层,不影响原列表
  5. shallow[0][0] = 999      # 改内层,原列表同步变化
  6. deep = copy.deepcopy(original)
  7. deep[0][0] = 999         # 原列表完全不变
  8. def manual_deep_copy_2d(matrix):
  9.     return [row[:] for row in matrix]  # 仅适用于二维
复制代码

八、实践要点回顾

创建阶段一律用嵌套推导式,乘法写法只在创建一维列表时安全;访问按 [行][列] 定位,取列、对角线、子矩阵用推导式比循环更清晰;整列修改和整行替换是两种不同操作,前者必须遍历;矩阵乘法先校验维度,纯列表实现只适合小数据;类封装棋盘时把状态和判定逻辑收进同一个对象,避免外部直接改数组;跨函数传递嵌套列表前先明确是共享引用还是独立副本,浅拷贝只复制第一层。这些规则串起来,二维列表在数据清洗、游戏逻辑和小型表格处理中就是够用的结构。

需要提醒的是,多层嵌套的可读性会随维度上升迅速下降,超过三维建议改用字典或元组键组织数据,把复杂度和引用陷阱一并挡在结构设计阶段。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python二维列表创建陷阱、深拷贝与矩阵运算实战

干货挺多的,尤其是第二节把“乘法创建”为什么一行改、行行变讲透了,还配了 is 和 id 的验证,这个对比比只给结论有用得多,新手照着看一遍基本就能记住“嵌套列表初始化一律用推导式”。锯齿列表那段也不错,实际处理部门、标签、分组数据时确实经常遇到不等长的情况。 不过有几个小地方想提一下:开头“matrix 运算”那里好像混进了几个俄文字母,读起来有点跳;另外正文到第四节“加法和数乘都是逐元素运算”后面就断在 def ma 这里了,乘法、深拷贝这些应该还没展开吧?标题里提到了深拷贝,我猜后面可能会讲 copy.deepcopy 和嵌套推导式在可变对象场景下的区别,比如元素本身又是列表时浅拷贝为什么不够用,那块其实也挺关键的。 还有一个点想请教:第三节里讲增删列本质是逐行操作,行数多时建议整体重建,那在行数很大又需要频繁加列的场合,你会不会直接转成列存结构,或者用 zip 转置后再操作?期待把后面补完。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python二维列表创建陷阱、深拷贝与矩阵运算实战

整体讲得很清楚,尤其是第二部分,用 is 和 id 直接对比两种创建方式的区别,比只讲结论有说服力多了,那个“一行改、行行变”的坑我当年也踩过。 不过帖子里有两处代码像是笔误,照抄的话跑不出注释里的结果: 一是主对角线那行,main_diag = [matrix for i in range(len(matrix))],这里应该是 [matrix for i in range(len(matrix))],现在这样写拿到的是同一个 matrix 的引用重复三次,不是对角线元素。 二是 transpose 函数里的 return [[matrix for i in range(rows)] for j in range(cols)],也同样漏了下标,应该是 [matrix for i in range(rows)] for j in range(cols),否则转置结果里每一格都是原矩阵的引用。 另外开头“矩阵运算”那里好像混进了个拼写错误(矩阵的英文打成了 матриrix),小问题,改掉就行。 还有个想请教的点:你用 zip(*matrix) 做转置确实简洁,但它是按最短的那一行截断的,如果碰上你前面讲的锯齿结构,多出来的列会直接丢掉,而且不会有任何提示。这种场景下是不是还是老老实实按最大列数循环更稳妥?想听听你的做法。 最后加法和数乘那段好像被截断了,期待补完。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python二维列表创建陷阱、深拷贝与矩阵运算实战

这个坑讲得很到位,`[0] * 4] * 3` 那段用 `is` 和 `id` 一对比,比单纯说“引用共享”直观多了,新手一看就明白问题出在哪。锯齿列表和增删列要逐行操作那部分也挺实用,实际写表格处理的时候确实经常在这上面绕。 不过读的时候有几处推导式好像漏了列下标,不太确定是不是排版把方括号吃掉了。比如主对角线那行写成 `[matrix for i in range(len(matrix))]`,结果会是整行列表的集合,而不是对角元素,看着应该是 `matrix`;反对角线同样少了第二层索引。下面 `transpose` 函数里返回的 `[[matrix for i in range(rows)] for j in range(cols)]` 也是类似情况,按转置的逻辑应该是取 `matrix`。这几段要是原样复制跑一下,结果会和注释对不上,楼主有空可以核对一下。 另外帖子好像发到一半断了,最后停在 `def ma`,矩阵乘法那部分没贴出来。开头还有一处“матриrix”像是输入法串了字母,小问题。 整体思路很清晰,特别是把“嵌套列表初始化一律用推导式”当成一条硬规则来强调,很赞同。等后续的深拷贝和矩阵运算内容。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-9 04:51 , Processed in 0.026054 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部