一维列表用熟之后,实际项目里真正难缠的往往是嵌套结构:电子表格是行×列,游戏棋盘是二维网格,图像像素也按通道×行×列排布。这类数据在 Python 中的基本载体就是二维列表——外层列表的每个元素又是一个列表。下面从创建方式讲起,重点拆解引用共享这个高频坑,再延伸到матриrix 运算、棋盘生成与表格处理等可直接复用的代码。
一、三种创建方式与它们的代价
小型固定数据直接写字面量最直观,索引规则是 matrix[行][列],负索引同样可用:
- matrix = [
- [1, 2, 3],
- [4, 5, 6],
- [7, 8, 9],
- ]
- print(matrix[0]) # [1, 2, 3]
- print(matrix[1][2]) # 6
- print(len(matrix)) # 3 行
- print(len(matrix[0])) # 3 列
复制代码
需要动态生成规则矩阵时,用嵌套推导式,外层控制行、内层控制列,顺序不能颠倒:
- zeros = [[0 for _ in range(4)] for _ in range(3)]
- table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
- n = 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,只是把这个对象的引用复制了三份,三行在内存中指向同一块数据。于是修改任意一行,其余行同步变化:
- wrong = [[0] * 4] * 3
- wrong[0][0] = 999
- print(wrong)
- # [[999, 0, 0, 0], [999, 0, 0, 0], [999, 0, 0, 0]]
- print(wrong[0] is wrong[1]) # True
- print(id(wrong[0]), id(wrong[1])) # 两个 id 完全相同
- correct = [[0] * 4 for _ in range(3)]
- correct[0][0] = 999
- print(correct)
- # [[999, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
- print(correct[0] is correct[1]) # False
复制代码
结论很明确:推导式每次循环都会执行一次内层 [0] * 4,得到独立的新列表;乘法只做引用复制。凡是嵌套列表的初始化,一律用推导式,不要用外层乘法。这个规则在三维及以上同样成立。
二维列表并不要求每行等长,锯齿结构在表示层级数据时反而更自然,例如部门与员工的从属关系:
- jagged = [
- [1, 2, 3],
- [4, 5],
- [6, 7, 8, 9],
- ]
- for i, row in enumerate(jagged):
- print(f'第{i}行有{len(row)}列: {row}')
复制代码
三、访问、遍历与常用行列操作
取单列、对角线都靠推导式完成,取子矩阵则可以先按行切片,再对每行做列切片:
- matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
- col_0 = [row[0] for row in matrix] # [1, 5, 9]
- main_diag = [matrix[i][i] for i in range(len(matrix))] # [1, 6, 11]
- anti_diag = [matrix[i][len(matrix) - 1 - i] for i in range(len(matrix))]
- def submatrix(matrix, row_start, row_end, col_start, col_end):
- return [row[col_start:col_end] for row in matrix[row_start:row_end]]
- print(submatrix(matrix, 0, 2, 1, 3)) # [[2, 3], [6, 7]]
复制代码
遍历推荐 enumerate 双重循环,既能拿到行列下标也能拿到值;如果只需要按顺序处理所有元素,可以用单层推导式展平:
- for i, row in enumerate(matrix):
- for j, value in enumerate(row):
- print(f'matrix[{i}][{j}] = {value}')
- flat = [cell for row in matrix for cell in row]
复制代码
修改方面,单个元素和整行都能直接赋值,整列必须循环,否则会替换掉整行对象:
- matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
- matrix[1][2] = 99 # 改单个元素
- matrix[0] = [10, 20, 30] # 换整行
- for row in matrix: # 改整列
- row[1] = 0
- matrix.append([7, 8, 9]) # 加一行
- for i, row in enumerate(matrix):
- row.append((i + 1) * 10) # 每行尾部追加,等效加一列
- del matrix[1] # 删一行
- for row in matrix:
- del row[-1] # 删一列
- matrix.insert(1, [100, 200, 300])
复制代码
增删列的本质是逐行操作,行数越多开销越大,批量场景下考虑整体重建而非原地修改。
四、矩阵转置、加法与乘法
转置用推导式可以写得很直白,新建 cols×rows 的结果矩阵,按下标搬运元素;更简洁的写法是借助 zip 解包:
- def transpose(matrix):
- if not matrix:
- return []
- rows, cols = len(matrix), len(matrix[0])
- return [[matrix[i][j] for i in range(rows)] for j in range(cols)]
- transposed2 = [list(col) for col in zip(*matrix)]
复制代码
加法和数乘都是逐元素运算,直接双层推导式即可:
- def matrix_add(a, b):
- rows, cols = len(a), len(a[0])
- return [[a[i][j] + b[i][j] for j in range(cols)] for i in range(rows)]
- def matrix_scalar_mul(matrix, scalar):
- return [[cell * scalar for cell in row] for row in matrix]
复制代码
乘法必须先校验维度,左矩阵的列数要等于右矩阵的行数,结果规模是 rows_a×cols_b,内层用 sum 完成点积:
- def matrix_multiply(a, b):
- if not a or not b:
- return []
- if len(a[0]) != len(b):
- raise ValueError('矩阵尺寸不匹配')
- rows_a, cols_a = len(a), len(a[0])
- cols_b = len(b[0])
- result = [[0] * cols_b for _ in range(rows_a)]
- for i in range(rows_a):
- for j in range(cols_b):
- result[i][j] = sum(a[i][k] * b[k][j] for k in range(cols_a))
- return result
- a = [[1, 2, 3], [4, 5, 6]] # 2×3
- b = [[7, 8], [9, 10], [11, 12]] # 3×2
- # 结果 2×2:[[58, 64], [139, 154]]
- # 校验:1*7 + 2*9 + 3*11 = 58
复制代码
三重循环的时间复杂度是 O(n³),纯列表只适合教学或小规模数据;真实数值计算应交给 NumPy 之类的库。
五、棋盘类应用:井字棋与扫雷
井字棋把棋盘封装成类,落子前判断格子是否为空,胜负检查覆盖三行、三列和两条对角线,最后用生成器判断平局:
- class TicTacToe:
- def __init__(self):
- self.board = [[' ' for _ in range(3)] for _ in range(3)]
- self.current_player = 'X'
- def make_move(self, row, col):
- if self.board[row][col] != ' ':
- return False
- self.board[row][col] = self.current_player
- self.current_player = 'O' if self.current_player == 'X' else 'X'
- return True
- def check_winner(self):
- b = self.board
- for row in b:
- if row[0] == row[1] == row[2] != ' ':
- return row[0]
- for col in range(3):
- if b[0][col] == b[1][col] == b[2][col] != ' ':
- return b[0][col]
- if b[0][0] == b[1][1] == b[2][2] != ' ':
- return b[0][0]
- if b[0][2] == b[1][1] == b[2][0] != ' ':
- return b[0][2]
- if all(cell != ' ' for row in b for cell in row):
- return '平局'
- return None
- def get_empty_cells(self):
- return [(i, j) for i in range(3) for j in range(3)
- if self.board[i][j] == ' ']
复制代码
扫雷棋盘生成分两步:用 set 记录已放置的雷坐标避免重复,再把每个雷周围的 8 个方向格子数值加一,边界用范围判断过滤:
- import random
- def create_minesweeper(rows, cols, mines):
- board = [[0 for _ in range(cols)] for _ in range(rows)]
- mine_positions = set()
- while len(mine_positions) < mines:
- r = random.randint(0, rows - 1)
- c = random.randint(0, cols - 1)
- mine_positions.add((r, c))
- board[r][c] = -1
- directions = [(-1,-1), (-1,0), (-1,1), (0,-1),
- (0,1), (1,-1), (1,0), (1,1)]
- for r, c in mine_positions:
- for dr, dc in directions:
- nr, nc = r + dr, c + dc
- if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != -1:
- board[nr][nc] += 1
- return board
复制代码
注意这里 board[r][c] 与周围计数必须在同一个棋盘对象上完成,如果误用乘法创建,所有行共享引用,数字会整体串位。
六、把二维列表当轻量表格用
CSV 文本按行切分、再按分隔符切列,就能得到二维列表;排序时保留表头,对表体用 lambda 指定列:
- def parse_csv_to_matrix(text, delimiter=','):
- lines = text.strip().split('\n')
- return [line.split(delimiter) for line in lines if line.strip()]
- def sort_by_column(data, col_index, numeric=False):
- headers = data[0]
- body = data[1:]
- if numeric:
- body.sort(key=lambda row: float(row[col_index]))
- else:
- body.sort(key=lambda row: row[col_index])
- return [headers] + body
复制代码
透视表的思路是先用集合收集行列维度并排序,建立值到下标的映射字典,再用字典按 (行值, 列值) 分组聚合,最后回填到一个预先推导出的零矩阵里:
- def pivot_table(data, row_field, col_field, value_field, agg_func=sum):
- row_values = sorted(set(d[row_field] for d in data))
- col_values = sorted(set(d[col_field] for d in data))
- pivot = [[0 for _ in range(len(col_values))] for _ in range(len(row_values))]
- row_idx = {v: i for i, v in enumerate(row_values)}
- col_idx = {v: i for i, v in enumerate(col_values)}
- groups = {}
- for record in data:
- key = (record[row_field], record[col_field])
- groups.setdefault(key, []).append(record[value_field])
- for (r, c), values in groups.items():
- pivot[row_idx[r]][col_idx[c]] = agg_func(values)
- return pivot, row_values, col_values
复制代码
七、三维列表与拷贝层级问题
三维列表就是多个二维平面的堆叠,索引顺序为 [平面][行][列]。创建时必须用三层推导式,任何一层出现乘法都会造成跨层引用共享:
- depth, rows, cols = 2, 3, 4
- cube = [[[0 for _ in range(cols)] for _ in range(rows)] for _ in range(depth)]
- cube[0][1][2] = 99
- # 只有平面0的第1行第2列变为99,其他位置保持0
- # 错误写法:[[[0]*3]*4]*2 平面间、行间都会共享引用
复制代码
拷贝问题同样按层级区分。切片、copy()、list()、copy.copy() 都只复制外层,内层对象仍是共享的;要彻底隔离必须用 copy.deepcopy,它对二维以上的结构才安全:
- import copy
- original = [[1, 2], [3, 4]]
- shallow = original[:]
- shallow.append([5, 6]) # 改外层,不影响原列表
- shallow[0][0] = 999 # 改内层,原列表同步变化
- deep = copy.deepcopy(original)
- deep[0][0] = 999 # 原列表完全不变
- def manual_deep_copy_2d(matrix):
- return [row[:] for row in matrix] # 仅适用于二维
复制代码
八、实践要点回顾
创建阶段一律用嵌套推导式,乘法写法只在创建一维列表时安全;访问按 [行][列] 定位,取列、对角线、子矩阵用推导式比循环更清晰;整列修改和整行替换是两种不同操作,前者必须遍历;矩阵乘法先校验维度,纯列表实现只适合小数据;类封装棋盘时把状态和判定逻辑收进同一个对象,避免外部直接改数组;跨函数传递嵌套列表前先明确是共享引用还是独立副本,浅拷贝只复制第一层。这些规则串起来,二维列表在数据清洗、游戏逻辑和小型表格处理中就是够用的结构。
需要提醒的是,多层嵌套的可读性会随维度上升迅速下降,超过三维建议改用字典或元组键组织数据,把复杂度和引用陷阱一并挡在结构设计阶段。 |