
1. 二维数组从概念到Python实现聊到Python里的二维数组很多刚入门的朋友可能会有点懵尤其是从C、Java这类语言转过来的。在那些语言里二维数组是一个内存连续、行列固定的规整结构声明int arr[3][4]编译器就给你划出12个整数的空间访问arr[i][j]就是一次地址计算。但在Python的世界里事情变得“动态”和“灵活”起来这种灵活有时反而成了初学者的第一个绊脚石。Python本身没有内置的、像C语言那样原生的“数组”类型。我们常说的“数组”在Python里通常指的是列表list。列表是个万能容器能装下任何类型的对象包括另一个列表。所以所谓的“二维数组”在Python中最直接的体现就是“列表的列表”list of lists。比如matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]]这看起来就是一个3行3列的矩阵。你可以通过matrix[1][2]来访问第二行第三列的元素结果是6语法上很直观。但问题就出在这个“列表的列表”上。因为它太灵活了所以创建和初始化时如果方法不对很容易掉进坑里。最常见的坑就是试图用[[0] * cols] * rows这种方式来创建一个全零的二维数组。看起来简洁明了对吧rows3, cols4arr [[0] * 4] * 3打印出来也是[[0,0,0,0], [0,0,0,0], [0,0,0,0]]。然而当你尝试修改其中一个元素时比如arr[0][1] 5你会发现整个第二列索引为1的列的所有行都变成了5[[0,5,0,0], [0,5,0,0], [0,5,0,0]]。这是因为*操作符在列表上是浅拷贝它复制的是对子列表的引用而不是创建新的子列表。arr里的三个元素实际上指向的是内存中同一个[0,0,0,0]列表对象。修改其中一个所有引用它的行都会跟着变。所以这篇内容就是来彻底讲清楚在Python里如何正确地、安全地、高效地创建和初始化各种你想要的二维数组。无论你是要做数值计算、图像处理、游戏地图还是任何需要网格状数据的场景这里的方法都能让你避开陷阱写出健壮的代码。我们会从最基础的列表推导式讲起覆盖到NumPy这种专业库的高性能操作并深入探讨不同初始化方式背后的内存模型和性能差异。2. 基础方法列表推导式与循环构造理解了“列表的列表”和浅拷贝的坑之后我们来看看正确的基础构造方法。最推荐、也最清晰的方式是使用列表推导式。2.1 使用列表推导式创建列表推导式不仅能避免引用陷阱而且代码意图非常清晰。创建一个rows行cols列初始值全为0的二维列表标准写法是rows, cols 3, 4 matrix [[0 for _ in range(cols)] for _ in range(rows)]我们来拆解一下这行代码。外层推导式[... for _ in range(rows)]负责创建行。for _ in range(rows)会循环rows次这里是3次每次循环执行[0 for _ in range(cols)]。内层推导式[0 for _ in range(cols)]负责创建每一行即一个子列表它会循环cols次这里是4次每次生成一个整数0。这样我们就得到了一个包含3个独立子列表的列表每个子列表包含4个独立的整数0。修改matrix[0][1] 5只会影响第一行第二列完全符合预期。为什么这是安全的因为每次执行[0 for _ in range(cols)]Python都会在内存中新建一个全新的列表对象。整数0在Python中是不可变对象所以即使内层推导式看起来也用了for _ in range(cols)但每次生成的0都是新的尽管小整数在Python中有缓存但逻辑上是独立的更重要的是外层循环每次迭代都会重新执行一次内层推导式从而生成一个全新的子列表。这就保证了每一行在内存中都是独立的。你可以把初始值0换成任何你需要的值比如None或者一个特定的字符串‘’。# 初始化一个3x4的二维列表元素为None matrix_none [[None for _ in range(cols)] for _ in range(rows)] # 初始化一个2x2的二维列表元素为字符串‘empty’ matrix_str [[‘empty‘ for _ in range(2)] for _ in range(2)]2.2 嵌套循环构造及其理解列表推导式本质上是嵌套循环的语法糖。如果你觉得推导式一下子不好理解可以先用标准的嵌套for循环写出来这有助于理解其创建过程rows, cols 3, 4 matrix [] for i in range(rows): row [] # 为每一行创建一个新的空列表 for j in range(cols): row.append(0) # 为当前行的每一列添加初始值 matrix.append(row) # 将创建好的行添加到矩阵中这个过程一目了然外层循环每次迭代都新建一个空的row列表内层循环负责填充这一行的每一列填充完成后把这一行row添加到外层的matrix中。因为每次row []都会在内存中新开辟空间所以每一行自然就是独立的。这种方法逻辑最清晰但代码行数较多在追求简洁的Python社区列表推导式是更地道的选择。2.3 一个经典的错误*操作符的陷阱再探我们再来深入看看那个经典错误arr [[0] * cols] * rows。为什么说它是“浅拷贝”的陷阱我们可以用id()函数查看对象的内存地址来验证rows, cols 3, 4 arr_wrong [[0] * cols] * rows print(“arr_wrong 中各行子列表的id“) for i, row in enumerate(arr_wrong): print(f“Row {i} id: {id(row)}“) # 输出结果会是三个相同的id说明它们指向同一个列表对象。你会发现三个行的id是完全一样的。而使用列表推导式创建的arr_correct [[0 for _ in range(cols)] for _ in range(rows)] print(“\narr_correct 中各行子列表的id“) for i, row in enumerate(arr_correct): print(f“Row {i} id: {id(row)}“)这里三个行的id则是各不相同的。这个陷阱在初始化嵌套的可变对象如列表、字典时尤为致命。所以一个必须牢记于心的原则当需要初始化一个包含可变对象的复合结构时避免使用*操作符进行复制务必使用列表推导式或循环来确保每个可变对象都是独立创建的。3. 进阶初始化填充特定模式与序列创建出结构正确的二维列表只是第一步。很多时候我们需要用特定的模式或序列来填充它而不是简单的同一个值。3.1 使用乘法与推导式结合进行部分优化对于像全零初始化这种非常基础且元素是不可变对象如整数、浮点数、字符串、元组的情况有一种写法在正确性和简洁性上做了折中rows, cols 3, 4 matrix [[0] * cols for _ in range(rows)]注意这里的关键变化是把* cols放在了内层推导式里。我们来分析for _ in range(rows)循环3次每次执行[0] * cols。[0] * 4会生成一个新的列表[0,0,0,0]。由于0是不可变整数这个操作是安全的并且每次循环都会执行一次[0] * cols从而生成一个新的子列表。所以这个写法也是正确的并且比双层嵌套的推导式[[0 for _ in range(cols)] for ...]在视觉上稍微简洁一点执行效率也可能微乎其微地高一点点因为列表的*乘法操作是C语言级别实现的。它同样保证了每一行的独立性。但是请务必注意其适用范围仅当内层元素是像0这样的不可变对象时这种写法才是安全的。如果你想初始化的元素本身是一个可变对象比如一个空列表[]那么[[]] * cols就会导致同一行内的所有列共享同一个子列表这又会产生类似的“列粘连”问题。对于可变对象的初始化最安全的依然是完整的内层推导式[[] for _ in range(cols)]。3.2 初始化递增序列或特定模式假设我们需要一个矩阵其元素值是行索引和列索引的某种函数例如value i * cols j按行展开的线性索引或者直接填充一个递增的序列。我们可以轻松地在推导式中加入计算逻辑rows, cols 3, 4 # 创建一个矩阵元素值为 row_index * cols col_index matrix_seq [[i * cols j for j in range(cols)] for i in range(rows)] print(matrix_seq) # 输出: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]这里内层推导式[i * cols j for j in range(cols)]利用了两个循环变量i和j来计算每个位置的值。你可以把i * cols j替换成任何复杂的表达式比如i ** 2 j、(i1) * (j1)乘法表或者根据i和j的奇偶性返回不同的值实现一个棋盘格图案。# 创建一个3x3的棋盘格0和1交替 checkerboard [[(i j) % 2 for j in range(3)] for i in range(3)] print(checkerboard) # 输出: [[0, 1, 0], [1, 0, 1], [0, 1, 0]]3.3 从一维列表或可迭代对象填充有时我们手头已经有一个扁平的一维列表比如从文件读取的一串数字想要按行或按列填充到一个二维结构中。按行填充相对简单我们可以利用列表的切片操作flat_list list(range(12)) # [0,1,2,...,11] rows, cols 3, 4 matrix_by_row [flat_list[i*cols : (i1)*cols] for i in range(rows)] print(matrix_by_row) # 输出: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]这里flat_list[i*cols : (i1)*cols]每次切片取出一行的数据。前提是flat_list的长度必须等于rows * cols。按列填充则稍微麻烦一些因为我们需要从一维列表中每隔rows个元素取一个。这需要更复杂的索引计算或者使用zip和迭代器技巧flat_list list(range(12)) rows, cols 3, 4 # 按列填充先创建结构再赋值 matrix_by_col [[0 for _ in range(cols)] for _ in range(rows)] for i in range(rows): for j in range(cols): matrix_by_col[i][j] flat_list[i j * rows] # 注意索引计算 print(matrix_by_col) # 输出: [[0, 3, 6, 9], [1, 4, 7, 10], [2, 5, 8, 11]]可以看到按列填充后矩阵的第一列是原列表的0,1,2第二列是3,4,5以此类推。这种索引转换在图像处理RGB通道分离、矩阵运算等场景中很常见。4. 专业之选使用NumPy库进行高效数组操作当你需要进行数值计算、线性代数运算、或者处理大规模的数值数据时Python原生的列表列表就显得力不从心了。它在内存存储上不连续每个子列表是独立对象存储的是Python对象整数对象开销大且没有向量化操作。这时NumPy库就是绝对的首选。NumPy提供了真正的、同构的、多维数组对象ndarray它在内存中是连续的存储的是原生数据类型如int32,float64并且支持广播和向量化操作速度比纯Python循环快几个数量级。4.1 安装与导入NumPy首先确保安装了NumPy。通常使用pip安装pip install numpy然后在代码中导入习惯上导入为npimport numpy as np4.2 使用np.zeros,np.ones,np.full初始化NumPy提供了非常直观的函数来创建具有初始值的数组。np.zeros(shape, dtypefloat): 创建指定形状的全零数组。shape可以是一个整数一维也可以是元组如(3, 4)表示3行4列。dtype指定数据类型默认为float64。arr_zeros np.zeros((3, 4)) print(arr_zeros) # 输出: # [[0. 0. 0. 0.] # [0. 0. 0. 0.] # [0. 0. 0. 0.]] print(arr_zeros.dtype) # float64 # 指定整数类型 arr_zeros_int np.zeros((2, 2), dtypeint) print(arr_zeros_int) # [[0 0], [0 0]]np.ones(shape, dtypefloat): 创建全1数组用法同np.zeros。arr_ones np.ones((2, 3)) print(arr_ones) # [[1. 1. 1.] # [1. 1. 1.]]np.full(shape, fill_value, dtypeNone): 创建用指定值fill_value填充的数组。这是最通用的初始化函数。arr_full np.full((3, 3), 7) # 3x3全部填充为7 print(arr_full) # [[7 7 7] # [7 7 7] # [7 7 7]] arr_full_none np.full((2, 2), None) # 填充为None对象 print(arr_full_none) # [[None None] # [None None]]4.3 使用np.arange和reshape创建序列数组np.arange类似于Python的range但生成的是NumPy数组。结合reshape方法可以方便地创建按顺序填充的二维数组。# 创建一个0到11的一维数组然后重塑为3行4列 arr_1d np.arange(12) # [0,1,2,...,11] arr_2d arr_1d.reshape((3, 4)) print(arr_2d) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]]reshape方法要求新形状的元素总数必须与原数组一致这里是12。你也可以直接链式调用arr_2d_direct np.arange(12).reshape(3, 4) # 参数可以不用元组直接写数字如果想按列优先Fortran风格填充可以使用order‘F‘参数arr_2d_col_major np.arange(12).reshape((3, 4), order‘F‘) print(arr_2d_col_major) # [[ 0 3 6 9] # [ 1 4 7 10] # [ 2 5 8 11]]4.4 使用np.random生成随机数组在机器学习、模拟仿真中经常需要随机初始化的矩阵。NumPy的random模块功能强大。np.random.rand(rows, cols): 生成在[0, 1)区间均匀分布的随机浮点数数组。rand_arr np.random.rand(2, 3) print(rand_arr) # 类似 [[0.548, 0.715, 0.603], [0.545, 0.424, 0.645]]np.random.randn(rows, cols): 生成标准正态分布均值为0标准差为1的随机数数组。randn_arr np.random.randn(2, 3)np.random.randint(low, high, size): 生成指定范围内的随机整数数组。int_arr np.random.randint(0, 10, size(3, 4)) # [0,10)的整数3x4np.random.random_sample(size): 功能同rand但参数是元组形式。arr np.random.random_sample((2, 2))4.5 NumPy数组与列表列表的转换虽然鼓励直接使用NumPy数组进行计算但有时也需要和原生列表进行交互。从列表列表创建NumPy数组使用np.array()函数。这是最常用的方式。list_of_lists [[1, 2, 3], [4, 5, 6]] np_arr np.array(list_of_lists) print(np_arr) # [[1 2 3] # [4 5 6]] print(type(np_arr)) # class ‘numpy.ndarray‘ print(np_arr.shape) # (2, 3)注意np.array()要求输入的列表列表是一个“矩形”即每一行的长度必须相同否则会创建一个一维的对象数组dtypeobject失去NumPy的性能优势。将NumPy数组转回列表列表使用tolist()方法。np_arr np.array([[1, 2], [3, 4]]) list_back np_arr.tolist() print(list_back) # [[1, 2], [3, 4]] print(type(list_back)) # class ‘list‘使用NumPy不仅初始化方便更重要的是它为后续的数学运算如矩阵乘法或np.dot、逐元素运算 - * /、统计函数np.sum,np.mean等提供了极其高效的支持。对于任何涉及数值计算的二维数组操作优先考虑NumPy。5. 特殊场景与性能考量掌握了基础和进阶方法后我们来看看一些特殊需求场景并深入探讨不同方法背后的性能差异这能帮助你在实际项目中做出更优选择。5.1 创建“锯齿状”数组非矩形数组Python的列表列表非常灵活它不强制要求所有子列表长度一致。这种“锯齿状”数组Jagged Array在某些场景下是有用的比如存储长度不一的序列。jagged [ [1, 2, 3], [4, 5], [6, 7, 8, 9] ] print(jagged[0]) # [1, 2, 3] print(jagged[1]) # [4, 5] print(jagged[2]) # [6, 7, 8, 9]创建锯齿状数组没有固定语法就是在构造时赋予不同长度的子列表即可。但需要注意的是NumPy数组不支持锯齿状结构。np.array(jagged)会得到一个一维的dtypeobject数组里面装着三个列表对象这通常不是你想要的结果也会丧失NumPy的效能。np_jagged np.array(jagged) print(np_jagged) # [list([1, 2, 3]) list([4, 5]) list([6, 7, 8, 9])] print(np_jagged.shape) # (3,) print(np_jagged.dtype) # object所以如果你的数据结构本身就是不规则的那么使用Python原生列表更合适。如果规则则用NumPy数组以获得性能。5.2 初始化一个“空”的二维列表有时我们需要先创建一个“架子”即指定行数和列数但暂时不填充任何值或者用None占位。这通常用于后续按索引赋值。用列表推导式创建None占位的数组是最清晰的方式rows, cols 5, 5 empty_matrix [[None for _ in range(cols)] for _ in range(rows)]这里用None表示“空位”。注意在Python中None是一个特殊的单例对象表示没有值。它和0、‘’空字符串是不同的。5.3 不同初始化方法的性能对比对于大规模数据初始化方式的性能差异不容忽视。我们简单对比一下几种创建1000x1000全零二维结构的方法。错误方法引用陷阱[[0] * 1000] * 1000—— 最快但完全错误不能用。双层列表推导式[[0 for _ in range(1000)] for _ in range(1000)]—— 纯Python循环较慢。乘法推导式[[0] * 1000 for _ in range(1000)]—— 利用了列表乘法的C级优化比纯双层推导式快。NumPyzerosnp.zeros((1000, 1000), dtypeint)—— 最快因为是C/C/Fortran底层实现内存连续。我们可以用timeit模块简单测试这里仅示意结论import timeit import numpy as np setup_py “““ rows, cols 1000, 1000 “““ setup_np “““ import numpy as np rows, cols 1000, 1000 “““ stmt1 “[[0 for _ in range(cols)] for _ in range(rows)]“ stmt2 “[[0] * cols for _ in range(rows)]“ stmt3 “np.zeros((rows, cols), dtypeint)“ # 执行多次取平均时间此处为示例实际运行时间因环境而异 # time_py1 timeit.timeit(stmt1, setupsetup_py, number10) # time_py2 timeit.timeit(stmt2, setupsetup_py, number10) # time_np timeit.timeit(stmt3, setupsetup_np, number10)实际测试中stmt2乘法推导式通常会比stmt1纯推导式快30%-50%。而stmt3NumPy会比最快的纯Python方法还要快几十到上百倍。这不仅仅是初始化速度的差异后续任何涉及数组的运算NumPy的向量化操作都会带来指数级的性能提升。因此性能选择的结论很明确小规模数据、简单逻辑、不想引入外部依赖使用[[0] * cols for _ in range(rows)]对于不可变初始值或双层列表推导式对于可变初始值或复杂计算。中大规模数值计算、追求极致性能毫不犹豫地使用NumPy。5.4 内存布局的思考行优先与列优先这是一个更深层次但很重要的点尤其在涉及性能优化或与C/Fortran代码交互时。内存是线性的二维数组在内存中也是按顺序存储的。行优先C-order最常用的方式。元素按行存储第一行存完再存第二行。a[0,0], a[0,1], ..., a[0,n-1], a[1,0], a[1,1], ...。Python列表列表和NumPy默认的存储顺序就是行优先。遍历时先循环行外层循环再循环列内层循环会获得更好的缓存局部性速度更快。# 行优先遍历更快 for i in range(rows): for j in range(cols): do_something(matrix[i][j])列优先Fortran-order元素按列存储第一列存完再存第二列。a[0,0], a[1,0], ..., a[m-1,0], a[0,1], a[1,1], ...。某些科学计算库或特定场景下使用。在NumPy中可以通过order‘F‘参数指定。arr_f np.array([[1,2],[3,4]], order‘F‘)对于纯Python列表列表没有直接的“顺序”概念因为每个子列表是独立对象。但遍历顺序依然会影响性能因为访问matrix[i]获取子列表引用是O(1)操作而跳行访问可能会稍微影响缓存。不过这种影响通常远小于NumPy数组在连续内存上顺序访问带来的差异。理解内存布局有助于你写出对缓存更友好的代码特别是在处理超大矩阵时按内存顺序遍历可以带来显著的性能提升。在NumPy中使用.flatten()或.ravel()方法展平数组时也可以指定order‘C‘默认行优先或order‘F‘列优先。