NumPy 1.20.1 版本发布详解:快速修复 distutils 与 random.shuffle 回归问题
【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy
NumPy 1.20.1 是 1.20 系列的首个快速修复(bugfix)版本,专门用于修复 1.20.0 发布后社区报告的一系列缺陷与回归问题。本篇以官方发布说明(doc/source/release/1.20.1-notes.rst)为主体,结合本仓库源码与测试用例,逐项剖析本次修复的根因、实现细节与验证方式,帮助读者了解 NumPy 版本发布流程中 bugfix 分支的运作机制,以及这些修复对下游用户的实际影响。
版本定位:面向回归的快速修复版
NumPy 1.20.1 不是一个功能版本,而是紧跟在 1.20.0 之后的快速 bugfix 版本(rapid bugfix release)。官方说明明确指出,该版本的目标是修复 1.20.0 发布后报告的几个 bug 与回归问题(regressions):
- 修复了导致下游项目出问题的 distutils bug;
- 修复了
random.shuffle的回归问题。
本次发布共合并 15 个 pull request,共有 8 位贡献者参与,其中 Nicholas McKibben 与 @Aerysv 为首次贡献补丁的新人(名字旁带 "+" 标记)。从 PR 的构成看,本次修复横跨构建系统(distutils、threads.h探测、f2py)、随机数子系统(shuffle、C 链接)、核心函数(isclose/allclose与 timedelta)以及类型注解与 CI 基础设施等多个方向,下面逐一展开。
Highlights 一:distutils 构建问题修复
问题背景:迭代中修改 fake libraries 列表
1.20.0 引入的 distutils 缺陷会破坏下游项目的构建。修复该问题的 PR 标题为"BUG: don't mutate list of fake libraries while iterating over..."(PR #18326),其核心是:distutils 在解析依赖时维护一个"伪造库"(fake libraries)列表,而原实现在遍历该列表的过程中直接对其进行了修改,这违反了"迭代时禁止修改容器"的基本约束,导致在特定链接场景下下游项目构建失败。
从修复类型(BUG)与后续 1.20.x 系列发布说明的稳定性可以看出,这类构建路径缺陷对使用 NumPy 构建链的第三方包影响最大。若下游项目恰好命中该遍历逻辑,升级到 1.20.0 后会出现链接错误,而 1.20.1 通过改为先收集、后修改的方式(从代码结构看,即遍历副本或延迟收集待处理项)恢复了正常的依赖解析流程。
相关构建修复:signed_char与threads.h探测
同属构建系统的还有两个修复:
- PR #18351("BUG: Fix missing signed_char dependency. Closes #18335"):修复了
signed_char依赖缺失的问题,该缺陷由 issue #18335 报告,会导致特定平台上的类型映射不完整; - PR #18357("BUG: threads.h existence test requires GLIBC > 2.12"):修正了
threads.h存在性探测的逻辑。原探测条件过于宽松,未考虑 glibc 版本下限,修复后要求GLIBC > 2.12才判定threads.h可用,避免在较老 glibc 环境下误判导致编译期问题。
这两项修复共同保证了 1.20.1 在更多 Linux 发行版与交叉编译场景下的可构建性。
Highlights 二:random.shuffle回归修复
random.shuffle回归是本版本另一个核心亮点。官方说明将其单独列出,本仓库源码与测试为我们提供了完整的证据链。
回归一:无法优雅处理 memoryview(gh-18273)
在 numpy/random/tests/test_random.py 中可以看到专门为该回归添加的回归测试:
def test_shuffle_memoryview(self): # gh-18273 # allow graceful handling of memoryviews # (treat the same as arrays) rng = random.RandomState(self.seed) a = np.arange(5).data rng.shuffle(a) assert_equal(np.asarray(a), [0, 1, 4, 3, 2]) rng = random.RandomState(self.seed) rng.shuffle(a) assert_equal(np.asarray(a), [0, 1, 2, 3, 4]) rng = np.random.default_rng(self.seed) rng.shuffle(a) assert_equal(np.asarray(a), [4, 1, 0, 3, 2])该测试同时覆盖了两套 API:
- 传统 API:
np.random.RandomState.shuffle; - 新 API:
np.random.default_rng().shuffle(即Generator.shuffle)。
测试断言对 memoryview(这里是np.arange(5).data,即ndarray的 buffer 内存视图)调用shuffle后,底层的数组内容被正确、可复现地打乱,且两次使用相同种子 shuffle 的结果相互还原。修复 PR #18327("MAINT: gracefully shuffle memoryviews")使得 memoryview 被"当作数组一样"优雅处理,而非抛出异常或产生未定义行为。
回归二:随机分布函数缺少 C 链接(PR #18328)
PR #18328("BUG: Use C linkage for random distributions")修复了随机分布函数缺失 C linkage 的问题。在 numpy/random/mtrand.pyx 中,shuffle的实现展现了其底层机制:
def shuffle(self, object x): ... if isinstance(x, np.ndarray) and not x.flags.writeable: raise ValueError('array is read-only') if type(x) is np.ndarray and x.ndim == 1 and x.size: # Fast, statically typed path: shuffle the underlying buffer. # Only for non-empty, 1d objects of class ndarray (subclasses such # as MaskedArrays may not support this approach). x_ptr = np.PyArray_BYTES(x) stride = x.strides[0] itemsize = x.dtype.itemsize # As the array x could contain python objects we use a buffer # of bytes for the swaps to avoid leaving one of the objects # within the buffer and erroneously decrementing it's refcount # when the function exits. buf = np.empty(itemsize, dtype=np.int8) # GC'd at function exit buf_ptr = np.PyArray_BYTES(buf) with self.lock: # We trick gcc into providing a specialized implementation for # the most common case, yielding a ~33% performance improvement. if itemsize == sizeof(np.npy_intp): self._shuffle_raw(n, sizeof(np.npy_intp), stride, x_ptr, buf_ptr) else: self._shuffle_raw(n, itemsize, stride, x_ptr, buf_ptr)要点解析:
- 只读数组保护:对不可写(
writeable=False)的数组直接抛出ValueError; - 一维 ndarray 快速路径:仅对"恰好是
ndarray类型(而非子类)、一维、非空"的输入走静态类型化的底层 buffer 交换路径,因为 MaskedArray 等子类可能不支持该方案; - 引用计数安全:交换使用
int8字节缓冲,避免在交换过程中让 Python 对象残留在缓冲区内、函数退出时被错误地减少引用计数; - 线程锁:
with self.lock保证并发场景下随机状态不被破坏; - 性能优化:当
itemsize == sizeof(np.npy_intp)(最常见情况)时,借助 gcc 生成专用实现,可获得约 33% 的性能提升。
PR #18328 修复的正是这些 C 层分布函数(含_shuffle_raw底层实现)在 C++ 编译环境下因缺少extern "C"链接约定而可能出现的链接失败或符号错乱问题。该问题在混合编译(C/C++ 混编)的构建链中尤为隐蔽。
相关测试:对象数组不被打散、只读保护
同样在 numpy/random/tests/test_random.py 中,还有两类相关测试:
test_shuffle_no_object_unpacking:对dtype=object数组 shuffle 后,数组中元素的id()必须保持不变(即不误拆解对象、不误改引用计数)。测试还覆盖了ndarray子类场景:传统 API(RandomState)对一维数组子类调用 shuffle 时会发出UserWarning(提示"Shuffling a one dimensional array..."),而新 API(Generator)则给出正确结果;test_shuffle_not_writeable:对只读数组 shuffle 必须抛出ValueError,匹配信息为'read-only'。
这些测试共同锁定了 1.20.1 中shuffle的行为契约,防止后续版本再次引入同类回归。
其余 13 个修复:从核心函数到 f2py、类型注解与 CI
核心函数:isclose/allclose与 timedelta64 的兼容
PR #18337("BUG: Allow unmodified use of isclose, allclose, etc. with timedelta")允许isclose、allclose等函数在不修改参数的情况下直接用于timedelta64类型。
在 numpy/_core/numeric.py 与 numpy/_core/numeric.py 中可以看到这两个函数的定义:
def allclose(a, b, rtol=1.e-5, atol=1.e-8, equal_nan=False): """ Returns True if two arrays are element-wise equal within a tolerance. The tolerance values are positive, typically very small numbers. The relative difference (`rtol` * abs(`b`)) and the absolute difference ... """ def isclose(a, b, rtol=1.e-5, atol=1.e-8, equal_nan=False): """ Returns a boolean array where two arrays are element-wise equal within a tolerance. ... """修复前,对timedelta64数组直接调用isclose/allclose会因内部类型处理不当而失败或需用户手工改 dtype;修复后用户可以直接使用默认参数比较时间间隔数组,无需任何预处理。这对涉及时间序列、采样间隔比较的科学计算场景是实质性的可用性改进。
DType 序列化:允许 pickle 所有相关 DType 类型
PR #18345("BUG: Allow pickling all relevant DType types/classes")修复了部分 DType 类型/类无法被 pickle 的问题。在 1.20 系列中,DType 系统正经历向新 DType 协议(相关设计参见 doc/neps/nep-0041-improved-dtype-support.rst)过渡,本次修复保证了所有相关 DType 类型与类均可被正常序列化,避免使用multiprocessing或pickle传递 dtype 时出现TypeError。
f2py:修复包装 F90 子例程的缺陷
PR #18354("BUG: Fix f2py bugs when wrapping F90 subroutines")与 PR #18356("MAINT: crackfortran regex simplify")共同处理了 f2py 相关问题:
- #18354 修复了将 Fortran 90 子例程包装为 Python 可调用模块时的多个 bug;
- #18356 简化了
crackfortran中的正则表达式逻辑(crackfortran是 f2py 用于解析 Fortran 源码的核心模块,实现位于 numpy/f2py/crackfortran.py)。
对 Fortran 用户而言,升级 1.20.1 后可更可靠地包装 F90 代码。
类型注解与__init__修复
- PR #18306("MAINT: Add missing placeholder annotations"):补充了缺失的占位类型注解(placeholder annotations),完善
numpy的__init__.pyi/_core相关类型桩文件(如 numpy/_core/init.pyi),对 mypy/pyright 用户更友好; - PR #18310("BUG: Fix typo in
numpy.__init__.py"):修复了 numpy/init.py 中的一个拼写错误,该错误在特定导入路径下可能引发问题。
CI 与文档维护
- PR #18336("CI: fix when GitHub Actions builds trigger, and allow ci skips"):修正 GitHub Actions 构建的触发时机,并允许通过提交信息跳过 CI;
- PR #18353("CI: CircleCI seems to occasionally time out, increase the limit"):CircleCI 偶发超时,提高超时上限;
- PR #18352("DOC: Change license date 2020 -> 2021"):将许可证版权年份由 2020 更新为 2021。
发布收尾
PR #18359("REL: Prepare for the NumPy 1.20.1 release")是标准的发布准备提交,负责更新版本号与发布元数据,标志着 1.20.1 分支冻结并进入发布流程。
升级建议与验证方式
对于使用 NumPy 1.20.0 的用户,尤其是以下三类场景,建议尽快升级到 1.20.1:
- 下游 C/C++/Fortran 扩展构建者:命中 distutils fake libraries 遍历缺陷、
signed_char依赖缺失或threads.h误判的平台,升级后可恢复稳定构建; - 随机数重度用户:对 memoryview 或
dtype=object数组调用shuffle、或在混编环境下使用RandomState/Generator的程序,升级后行为恢复正确; - 时间序列分析者:需要对
timedelta64数组直接使用isclose/allclose的代码。
升级后可通过仓库自带测试集验证核心行为(需要已构建的开发环境):
python -m pytest numpy/random/tests/test_random.py -k "shuffle" python -m pytest numpy/random/tests/test_random.py -k "memoryview or not_writeable"第一条命令覆盖shuffle的回归测试(含 memoryview、对象数组、只读保护),第二条进一步聚焦 memoryview 与只读数组场景;isclose/allclose对 timedelta 的验证可参考 numpy/_core/numeric.py 的文档示例运行。
小结
NumPy 1.20.1 虽然规模不大(15 个 PR、8 位贡献者),但精准地解决了 1.20.0 带来的两个高影响回归——distutils 构建缺陷与random.shuffle回归——并顺带修复了 f2py、threads.h探测、DType pickling、timedelta 比较等一系列问题。从本仓库源码与测试可以看到,每一个 BUG 修复都伴随对应的回归测试(如 numpy/random/tests/test_random.py 中的test_shuffle_memoryview),这正是 NumPy 版本发布流程中"快速修复 + 回归防护"机制的典型体现。对于仍停留在 1.20.0 或更早版本的用户,本版本是低风险、高收益的升级目标。
【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考