NumPy 基础语法
本章将介绍 NumPy 的核心概念,包括 ndarray 对象、数据类型、数组属性等基础知识。
ndarray 对象简介
ndarray(n-dimensional array)是 NumPy 的核心对象,它是一个多维数组容器。与 Python 原生的列表相比,ndarray 在存储效率和运算速度上都有显著优势。
为什么 NumPy 数组更快?
连续的内存存储:ndarray 在内存中是连续存储的,而 Python 列表存储的是对象的引用。这种存储方式使得数组元素可以快速访问。
固定的数据类型:ndarray 中的所有元素必须是相同的数据类型,这允许更高效的存储和计算。
优化的 C 实现:NumPy 的核心是用 C 语言编写的,执行效率远高于纯 Python 代码。
SIMD 指令支持:NumPy 可以利用 CPU 的 SIMD(单指令多数据)指令进行并行计算。
创建数组
从 Python 列表创建
最基本的方式是从 Python 列表创建数组:
import numpy as np
# 一维数组
a = np.array([1, 2, 3, 4, 5])
print(f"一维数组: {a}")
print(f"形状: {a.shape}")
# 二维数组
b = np.array([[1, 2, 3], [4, 5, 6]])
print(f"\n二维数组:\n{b}")
print(f"形状: {b.shape}")
# 指定数据类型
c = np.array([1, 2, 3], dtype=np.float32)
print(f"\nfloat32 数组: {c}")
print(f"数据类型: {c.dtype}")
使用内置函数创建
NumPy 提供了多种创建数组的便捷函数:
import numpy as np
# arange: 类似 range,生成等差数组
arr1 = np.arange(0, 10, 2) # 0, 2, 4, 6, 8
print(f"arange: {arr1}")
# linspace: 生成等间隔的数组
arr2 = np.linspace(0, 1, 5) # 0, 0.25, 0.5, 0.75, 1
print(f"linspace: {arr2}")
# zeros: 全零数组
arr3 = np.zeros((3, 4)) # 3x4 的全零矩阵
print(f"zeros:\n{arr3}")
# ones: 全一数组
arr4 = np.ones((2, 3)) # 2x3 的全一矩阵
print(f"ones:\n{arr4}")
# eye: 单位矩阵
arr5 = np.eye(3) # 3x3 单位矩阵
print(f"eye:\n{arr5}")
# full: 指定值的数组
arr6 = np.full((2, 3), 99) # 2x3 的全是 99 的矩阵
print(f"full:\n{arr6}")
# random: 随机数组
arr7 = np.random.rand(3, 2) # 3x2 的 [0, 1) 均匀分布随机数
print(f"random.rand:\n{arr7}")
arr8 = np.random.randn(3, 2) # 3x2 的标准正态分布随机数
print(f"random.randn:\n{arr8}")
arr9 = np.random.randint(0, 10, (3, 3)) # 3x3 的 [0, 10) 整数随机数
print(f"random.randint:\n{arr9}")
数据类型
NumPy 支持多种数据类型,常见的有:
| dtype | 说明 | 取值范围 |
|---|---|---|
np.int8 | 8位有符号整数 | -128 到 127 |
np.int16 | 16位有符号整数 | -32768 到 32767 |
np.int32 | 32位有符号整数 | -2147483648 到 2147483647 |
np.int64 | 64位有符号整数 | 很大范围 |
np.uint8 | 8位无符号整数 | 0 到 255 |
np.float16 | 16位浮点数 | 半精度 |
np.float32 | 32位浮点数 | 单精度 |
np.float64 | 64位浮点数 | 双精度(默认) |
np.bool_ | 布尔类型 | True / False |
np.complex64 | 复数(两个32位) | - |
np.complex128 | 复数(两个64位) | - |
数据类型指定与转换
import numpy as np
# 创建时指定类型
arr1 = np.array([1, 2, 3], dtype=np.float32)
print(f"指定类型: {arr1.dtype}")
# 类型转换
arr2 = np.array([1.5, 2.7, 3.9])
arr3 = arr2.astype(np.int32) # 转换为整数
print(f"转换前: {arr2}, 转换后: {arr3}")
# 查看数组类型
print(f"数组类型: {arr2.dtype}")
dtype 参数详解
创建数组时,dtype 参数可以接受多种形式的类型指定:
import numpy as np
# 使用字符串
arr1 = np.array([1, 2, 3], dtype='float32')
# 使用 NumPy 类型
arr2 = np.array([1, 2, 3], dtype=np.float64)
# 使用 Python 内置类型(会被映射到 NumPy 类型)
arr3 = np.array([1, 2, 3], dtype=float) # 等同于 float64
# 复数类型
arr4 = np.array([1+2j, 3+4j], dtype=np.complex128)
# 字符串类型
arr5 = np.array(['hello', 'world'], dtype='U10') # Unicode 字符串,最多10个字符
# 字节串类型
arr6 = np.array([b'hello', b'world'], dtype='S10') # 字节串,最多10字节
数组属性
ndarray 对象有多个重要属性,用于描述数组的特征:
import numpy as np
# 创建示例数组
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
print(f"数组:\n{arr}")
print(f"ndim(维度数): {arr.ndim}") # 2
print(f"shape(形状): {arr.shape}") # (2, 3)
print(f"size(元素总数): {arr.size}") # 6
print(f"dtype(数据类型): {arr.dtype}") # float32
print(f"itemsize(元素字节大小): {arr.itemsize}") # 4
print(f"nbytes(总字节数): {arr.nbytes}") # 24
print(f"flags(内存信息):\n{arr.flags}")
属性的实际意义
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]])
# ndim: 数组的维度
print(f"一维数组维度: {np.array([1, 2, 3]).ndim}") # 1
print(f"二维数组维度: {arr.ndim}") # 2
print(f"三维数组维度: {np.array([[[1], [2]], [[3], [4]]]).ndim}") # 3
# shape: 每个维度的元素数量
print(f"shape: {arr.shape}") # (2, 3) 表示2行3列
# size: 所有元素的总数
print(f"size: {arr.size}") # 6
# 计算维度大小时
print(f"2x3x4 数组的大小: {np.zeros((2, 3, 4)).size}") # 24
数组的内存布局
理解 NumPy 数组的内存布局对于编写高性能代码至关重要。这部分内容将深入解释 ndarray 在内存中的存储方式。
内存模型概述
ndarray 的核心是一个连续的一维内存块,加上一个索引方案将 N 维索引映射到内存位置。这种设计使得 NumPy 能够高效地处理多维数组,同时保持内存的连续性。
对于一个形状为 (d0, d1, ..., dN-1) 的 N 维数组,要访问位置 (n0, n1, ..., nN-1) 的元素,NumPy 使用 strides(步幅) 来计算内存偏移量:
其中 是第 k 维的步幅(stride),表示在该维度移动一个索引需要跨越多少字节。
Strides 的深入理解
strides 是理解 NumPy 数组内存布局的关键。它决定了如何从多维索引映射到内存地址。
import numpy as np
# 创建一个简单的二维数组
arr = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.int32) # int32 每个元素 4 字节
print(f"数组:\n{arr}")
print(f"形状: {arr.shape}") # (2, 3) - 2行3列
print(f"步幅: {arr.strides}") # (12, 4)
# strides 的含义:
# strides[0] = 12: 移动到下一行需要跨越 12 字节(3个元素 * 4字节)
# strides[1] = 4: 移动到下一列需要跨越 4 字节(1个元素 * 4字节)
手动计算元素位置:理解 strides 让你能预测数组在内存中的布局。
import numpy as np
arr = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.int32)
# 手动计算元素 arr[1, 2](值为 6)的内存偏移
row_stride, col_stride = arr.strides
row_idx, col_idx = 1, 2
# 偏移量 = row_idx * row_stride + col_idx * col_stride
offset = row_idx * row_stride + col_idx * col_stride
print(f"arr[1, 2] 的内存偏移: {offset} 字节")
# 验证:使用 ctypes 获取实际地址
import ctypes
base_addr = arr.ctypes.data
element_addr = base_addr + offset
element_value = ctypes.cast(element_addr, ctypes.POINTER(ctypes.c_int32)).contents.value
print(f"通过地址访问的值: {element_value}")
行优先与列优先
NumPy 支持两种主要的内存布局方式,这直接影响了 strides 的值:
C 顺序(行优先,Row-major):默认方式。数组按行存储,同一行的元素在内存中连续。这是 C 语言的存储方式。
Fortran 顺序(列优先,Column-major):数组按列存储,同一列的元素在内存中连续。这是 Fortran 和 MATLAB 的存储方式。
import numpy as np
# 创建同样数据但不同顺序的数组
arr_c = np.array([[1, 2, 3], [4, 5, 6]], order='C')
arr_f = np.array([[1, 2, 3], [4, 5, 6]], order='F')
print("C 顺序(行优先):")
print(f" 数组:\n{arr_c}")
print(f" strides: {arr_c.strides}") # (24, 8) - 每行24字节,每元素8字节(默认int64)
print("\nFortran 顺序(列优先):")
print(f" 数组:\n{arr_f}")
print(f" strides: {arr_f.strides}") # (8, 16) - 每元素8字节,每列16字节
# 展平顺序也不同
print(f"\nC 顺序展平: {arr_c.flatten()}")
print(f"F 顺序展平: {arr_f.flatten(order='F')}")
为什么顺序重要? 当你按数组的"自然顺序"访问元素时,CPU 可以更高效地利用缓存。对于 C 顺序数组,按行遍历更快;对于 Fortran 顺序数组,按列遍历更快。
import numpy as np
import time
# 创建一个大数组
n = 5000
arr_c = np.random.rand(n, n)
arr_f = np.asfortranarray(arr_c)
# C 顺序数组按行求和(快)
start = time.time()
_ = arr_c.sum(axis=1)
time_c_row = time.time() - start
# C 顺序数组按列求和(慢,因为跳着访问)
start = time.time()
_ = arr_c.sum(axis=0)
time_c_col = time.time() - start
# Fortran 顺序数组按列求和(快)
start = time.time()
_ = arr_f.sum(axis=0)
time_f_col = time.time() - start
print("内存布局对性能的影响:")
print(f" C顺序按行求和: {time_c_row:.4f}s")
print(f" C顺序按列求和: {time_c_col:.4f}s")
print(f" F顺序按列求和: {time_f_col:.4f}s")
连续性(Contiguity)
一个数组是"连续的"意味着它的所有元素在内存中占据一块连续的区域。连续数组可以更高效地传递给外部库(如 C 扩展),并且某些操作在连续数组上更快。
import numpy as np
# 创建连续数组
arr = np.arange(12).reshape((3, 4))
print(f"原始数组:")
print(f" C 连续: {arr.flags['C_CONTIGUOUS']}")
print(f" F 连续: {arr.flags['F_CONTIGUOUS']}")
print(f" strides: {arr.strides}")
# 切片通常创建非连续视图
sliced = arr[:, ::2] # 每隔一列取一列
print(f"\n切片 arr[:, ::2]:")
print(f" C 连续: {sliced.flags['C_CONTIGUOUS']}")
print(f" F 连续: {sliced.flags['F_CONTIGUOUS']}")
print(f" strides: {sliced.strides}") # 注意 strides 变了
# 为什么切片后 C_CONTIGUOUS 是 False?
# 因为元素在内存中不再连续排列(跳着选)
非连续数组的影响:
import numpy as np
import time
# 创建大数组并做切片
large = np.random.rand(10000, 10000)
sliced = large[:, ::2] # 非连续
# 比较操作速度
start = time.time()
_ = large.sum()
time_contiguous = time.time() - start
start = time.time()
_ = sliced.sum()
time_non_contiguous = time.time() - start
print(f"连续数组求和: {time_contiguous:.4f}s")
print(f"非连续数组求和: {time_non_contiguous:.4f}s")
print(f"速度差异: {time_non_contiguous/time_contiguous:.2f}x")
强制转换为连续数组
当需要将非连续数组转换为连续数组时,可以使用以下方法:
import numpy as np
arr = np.arange(12).reshape((3, 4))
sliced = arr[:, ::2] # 非连续
print(f"切片后 C 连续: {sliced.flags['C_CONTIGUOUS']}")
# 方法1: np.ascontiguousarray(如果已连续则不复制)
contiguous = np.ascontiguousarray(sliced)
print(f"转换后 C 连续: {contiguous.flags['C_CONTIGUOUS']}")
# 方法2: np.copy(总是复制)
copied = sliced.copy()
print(f"copy 后 C 连续: {copied.flags['C_CONTIGUOUS']}")
# 方法3: .flatten() 后 reshape(展平再重塑)
reshaped = sliced.flatten().reshape(sliced.shape)
print(f"reshape 后 C 连续: {reshaped.flags['C_CONTIGUOUS']}")
数组标志(Flags)
ndarray 的 flags 属性提供了关于内存布局的详细信息:
import numpy as np
arr = np.arange(12).reshape((3, 4))
print("数组标志:")
print(f" C_CONTIGUOUS (C顺序连续): {arr.flags['C_CONTIGUOUS']}")
print(f" F_CONTIGUOUS (F顺序连续): {arr.flags['F_CONTIGUOUS']}")
print(f" OWNDATA (拥有数据): {arr.flags['OWNDATA']}")
print(f" WRITEABLE (可写): {arr.flags['WRITEABLE']}")
print(f" ALIGNED (内存对齐): {arr.flags['ALIGNED']}")
print(f" WRITEBACKIFCOPY: {arr.flags['WRITEBACKIFCOPY']}")
# 视图的 OWNDATA 为 False
view = arr[1:, :]
print(f"\n视图的 OWNDATA: {view.flags['OWNDATA']}")
print(f"视图的 base: {view.base is arr}")
各标志的含义:
| 标志 | 含义 |
|---|---|
C_CONTIGUOUS | 数据按 C 顺序连续存储 |
F_CONTIGUOUS | 数据按 Fortran 顺序连续存储 |
OWNDATA | 数组拥有自己的数据(不是视图) |
WRITEABLE | 数据可以被修改 |
ALIGNED | 数据在内存中正确对齐(提高访问效率) |
基本运算
算术运算
NumPy 的算术运算是元素级的(element-wise):
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 基本运算
print(f"a + b = {a + b}") # [5, 7, 9]
print(f"a - b = {a - b}") # [-3, -3, -3]
print(f"a * b = {a * b}") # [4, 10, 18]
print(f"a / b = {a / b}") # [0.25, 0.4, 0.5]
print(f"a ** 2 = {a ** 2}") # [1, 4, 9]
广播机制
广播(Broadcasting)是 NumPy 处理不同形状数组的方式,使得不同形状的数组可以进行运算:
import numpy as np
# 标量与数组运算
arr = np.array([1, 2, 3])
print(f"arr + 10 = {arr + 10}") # [11, 12, 13]
# 一维与二维数组运算
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([1, 2, 3])
print(f"\n二维 + 一维:\n{a + b}")
# 结果:
# [[2, 4, 6],
# [5, 7, 9]]
# 行列向量
a = np.array([[1, 2, 3], [4, 5, 6]])
row = np.array([[1, 2, 3]]) # shape: (1, 3)
col = np.array([[1], [2]]) # shape: (2, 1)
print(f"\n行向量加:\n{a + row}")
print(f"\n列向量加:\n{a + col}")
矩阵乘法
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# 元素级乘法
print(f"元素级乘法:\n{a * b}")
# 矩阵乘法(Python 3.5+)
print(f"矩阵乘法 @:\n{a @ b}")
# 使用 dot 函数
print(f"矩阵乘法 dot:\n{np.dot(a, b)}")
# 一维数组的内积
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
print(f"向量内积: {np.dot(v1, v2)}") # 32 (1*4 + 2*5 + 3*6)
比较运算和布尔索引
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
# 比较运算返回布尔数组
print(f"arr > 3: {arr > 3}") # [False False False True True]
print(f"arr == 3: {arr == 3}") # [False False True False False]
# 布尔索引:选取满足条件的元素
print(f"arr[arr > 3]: {arr[arr > 3]}") # [4, 5]
# 复合条件
print(f"arr[(arr > 2) & (arr < 5)]: {arr[(arr > 2) & (arr < 5)]}") # [3, 4]
# np.where 实现条件选择
result = np.where(arr > 3, arr * 2, arr)
print(f"np.where: {result}") # [1, 2, 3, 8, 10]
小结
本章介绍了 NumPy 的基础概念:
- ndarray 对象:NumPy 的核心数据结构,支持多维数组
- 创建数组:从列表创建、使用内置函数创建
- 数据类型:多种数值类型和字符串类型
- 数组属性:ndim、shape、size、dtype 等
- 基本运算:算术运算、广播机制、矩阵乘法
- 比较和布尔索引:用于数据筛选
这些是 NumPy 最基础的概念,掌握它们是后续学习的基础。下一章我们将学习数组的创建方法。