第 01 章开始之前
cnumpy 是一个原生的 Windows x64 DLL,提供公开的 C ABI 以及一个 AutoHotkey v2 门面。本教程中的一切都通过仓库里的两个文件运行:
| 组件 | 路径 |
|---|---|
| AutoHotkey 门面 | ahk\numpy.ahk |
| 合格发布 DLL | build\x64\Release\cnumpy_ahk.dll |
所有组件必须使用同一架构:Windows x64、64 位的
AutoHotkey v2 解释器(发布版以 2.1-alpha.30 x64 验证),以及 x64
版 DLL。请在第一次调用库之前设置 Numpy.DllPath ——
数组工厂方法会按需调用 Numpy.Init(),而 DLL
加载后再修改路径不会替换已加载的模块。
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
; 脚本位于仓库根目录时:
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
MsgBox "cnumpy " Numpy.Version()
cnumpy 1.21.0-cnumpy每个 NdArray 都拥有一个原生句柄。释放数组的方式是丢弃所有
AHK 引用(赋值为 0),并且只在最后一个数组消失之后才调用
Numpy.Cleanup()。第 10 章会完整讲解生命周期纪律;中间的
短示例为了可读性省略了这部分。
第 02 章基础
cnumpy 的核心对象是同质多维数组:一张由同一类型元素组成、以非负整数索引的表。 和 NumPy 一样,cnumpy 中的维度称为轴(axis)。
数组类是 Numpy.NdArray,其最重要的属性与 NumPy 一一对应:
| 属性 | 含义 |
|---|---|
Ndim | 轴(维度)的数量 |
Shape | 各维大小组成的 AHK Array(返回的是克隆) |
Size | 元素总数 |
Dtype | 元素类型 id,如 Numpy.DT_FLOAT64 = 13 |
ItemSize | 单个元素的字节数 |
Nbytes | 元素数据的总字节数 |
Strides、CContiguous、FContiguous | 内存布局 |
一个例子
a := Numpy.Arange(0, 15).Reshape([3, 5])
MsgBox a.ToString()
MsgBox "shape: [" a.Shape[1] ", " a.Shape[2] "]`n"
. "ndim: " a.Ndim "`n"
. "dtype: " Numpy.Typename(a.Dtype) "`n"
. "itemsize: " a.ItemSize "`n"
. "size: " a.Size
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]
shape: [3, 5]
ndim: 2
dtype: double precision
itemsize: 8
size: 15Numpy.Arange 默认产生 float64 元素,因此
Numpy.Typename(a.Dtype) 报告原生名称
double precision。Shape 返回的是克隆的 AHK Array ——
修改它不会改变原生数组的形状。
创建数组
Numpy.Array 接受一个扁平的 AHK 数值数组,外加可选的行主序形状。
数据长度必须恰好等于各维大小之积 —— 不存在静默截断或补齐:
vector := Numpy.Array([6, 7, 8]) ; 一维 float64,形状 [3]
matrix := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
ints := Numpy.IntArray([1, 2, 3]) ; int64 元素
booleans := Numpy.Array([1, 0, 1], [3], Numpy.DT_BOOL)
MsgBox matrix.ToString()
MsgBox ints.ToString()
MsgBox booleans.ToString()
[[1, 2, 3],
[4, 5, 6]]
[1, 2, 3]
[True, False, True]常用的 dtype 常量:
| AHK 常量 | 原生 dtype |
|---|---|
Numpy.DT_BOOL | bool |
Numpy.DT_INT32 | 有符号 32 位整数 |
Numpy.DT_LONGLONG | 有符号 64 位整数 |
Numpy.DT_FLOAT32 | 32 位浮点数 |
Numpy.DT_FLOAT64 | 64 位浮点数 —— 默认值 |
Numpy.DT_COMPLEX128 | 两个 64 位浮点分量 |
很多时候数组的元素未知而大小已知。cnumpy 提供与 NumPy 相同的占位工厂, 一律以形状为第一个参数:
z := Numpy.Zeros([3, 4]) ; float64 全零
o := Numpy.Ones([2, 3], Numpy.DT_INT32) ; int32 全一
e := Numpy.Empty([2, 3]) ; 未初始化
f := Numpy.Full([2, 2], 3.14) ; 常量填充
r := Numpy.Arange(10, 30, 5) ; [10, 15, 20, 25]
x := Numpy.Linspace(0, 2, 9) ; 0 到 2 共 9 个点
MsgBox r.ToString()
MsgBox x.ToString()
[10, 15, 20, 25]
[0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]当 Arange 需要浮点步长时,优先使用
Linspace:它接受元素个数而非步长,因此不受浮点步长
累积误差的影响。
打印数组
NdArray.ToString() 以嵌套方括号渲染数组:最后一个轴从左到右打印,
其余轴以换行分隔,布局与 NumPy 一致。布尔值渲染为
True/False,浮点数默认最多 8 位有效数字。
c := Numpy.Arange(0, 24).Reshape([2, 3, 4]) ; 三维数组
MsgBox c.ToString()
; Array2String 遵循打印选项,可对大数组省略显示:
previous := Numpy.SetPrintOptions(, 6) ; threshold := 6
big := Numpy.Arange(0, 10000)
MsgBox Numpy.Array2String(big, 4096)
Numpy.SetPrintOptions(, previous["threshold"]) ; 恢复
[[[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]]
[0, 1, 2, ..., 9997, 9998, 9999]ToString() 总是打印全部元素。
Numpy.Array2String(source, bufferSize) 会应用通过
Numpy.SetPrintOptions(precision, threshold, edgeitems, linewidth,
suppress) 设置的打印选项;该方法返回之前的设置以便恢复,省略的参数
保持当前值。要把数据取回纯 AHK,ToArray() 返回扁平的 AHK
数值 Array。
基本运算
算术运算逐元素进行并分配新数组。与 NumPy 的一个刻意差异:cnumpy
的运算接受两个 NdArray 操作数 —— AHK 数字不会被自动提升。
标量请用 Numpy.Full 包装(或用可广播的单元素数组):
a := Numpy.Array([20, 30, 40, 50])
b := Numpy.Arange(0, 4) ; [0, 1, 2, 3]
c := Numpy.Subtract(a, b)
squares := Numpy.Multiply(b, b)
tenSin := Numpy.Multiply(Numpy.Full([1], 10.0), Numpy.Sin(a))
mask := Numpy.Less(a, Numpy.Full([1], 35.0)) ; 比较 -> bool 数组
MsgBox c.ToString()
MsgBox squares.ToString()
MsgBox tenSin.ToString()
MsgBox mask.ToString()
[20, 29, 38, 47]
[0, 1, 4, 9]
[9.1294525, -9.8803162, 7.4511316, -2.6237485]
[True, True, False, False]乘法运算符是逐元素的;矩阵乘法用 Matmul(或
Dot),既有静态方法也有实例方法:
A := Numpy.Array([1, 1, 0, 1], [2, 2])
B := Numpy.Array([2, 0, 3, 4], [2, 2])
elementwise := Numpy.Multiply(A, B)
product := A.Matmul(B) ; 等价于 Numpy.Matmul(A, B)
MsgBox elementwise.ToString()
MsgBox product.ToString()
[[2, 0],
[0, 4]]
[[5, 4],
[3, 4]]归约运算位于数组上。不带轴参数时对全部元素归约并返回
AHK 数字;带轴参数时返回新数组。-1
表示最后一个轴(门面经由 v2 导出路由,因此 axis=None
与“最后一个轴”始终有区分):
rg := Numpy.Arange(0, 6).Reshape([2, 3]) ; [[0,1,2],[3,4,5]]
total := rg.Sum() ; AHK 数字:15.0
colSums := rg.Sum(0) ; NdArray,每列一个和
rowSums := rg.Sum(1) ; NdArray,每行一个和
running := rg.Cumsum(1) ; 沿每行的累积和
MsgBox total "`n" colSums.ToString() "`n" rowSums.ToString()
MsgBox running.ToString()
15.0
[3, 5, 7]
[3, 12]
[[0, 1, 3],
[3, 7, 12]]同样的模式覆盖 Min、Max、Mean、
Std、Var、Prod、Argmax、
Argmin、Any、All、Median、
Percentile,以及各自忽略 NaN 的 Nan* 变体。
通用函数
常见数学函数逐元素运算并产生新数组。它们既是 Numpy.*
静态方法,也是 NdArray 实例方法:
b := Numpy.Arange(0, 3) ; [0, 1, 2]
MsgBox b.Exp().ToString()
MsgBox b.Sqrt().ToString()
c := Numpy.Array([2, -1, 4])
MsgBox Numpy.Add(b, c).ToString()
[1, 2.7182818, 7.3890561]
[0, 1, 1.4142136]
[2, 0, 6]可用的函数族包括三角函数(Sin、Cos、
Arctan2、Hypot……)、指数与对数(Exp、
Log、Log2、Log1p……)、取整
(Floor、Ceil、Rint、Trunc、
Around)、比较与极值(Maximum、Minimum、
Fmax)、逻辑与位运算,以及特殊函数(Erf、
Gamma、I0……)。
支持显式目标数组的运算可以完全避免结果分配 —— 目标的形状、dtype 和布局会被校验,绝不会被替换:
out := Numpy.Empty([3])
Numpy.Add(b, c, out) ; 写入 out 并返回它
Numpy.Sqrt(b, out) ; Sqrt 同理
索引、切片与迭代
cnumpy 中的元素访问是显式的,两套约定值得一次性记牢:
arr.GetItem(i)/arr.SetItem(i, v)使用 0 起始的扁平索引,类似 NumPy 的arr.item(i)。- 方括号语法糖
arr[i]是 1 起始的,与 AutoHotkey 惯例一致 ——arr[1]是第一个元素。 Numpy.ArrayGetItem(arr, [i, j, …])每个维度取一个 0 起始索引(负索引从末尾数起),返回 0 维数组。
cubes := Numpy.Power(Numpy.Arange(0, 10), Numpy.Full([1], 3.0))
MsgBox cubes.ToString()
MsgBox cubes.GetItem(2) ; 0 起始扁平访问 -> 8.0
MsgBox cubes[3] ; 1 起始 AHK 语法糖 -> 8.0
part := Numpy.Slice(cubes, 2, 5) ; 元素 2..4,相当于 a[2:5]
MsgBox part.ToString()
everyOther := Numpy.Slice(cubes, 0, 10, 2) ; 相当于 a[0:10:2]
MsgBox everyOther.ToString()
reversed := Numpy.Flip(cubes) ; 相当于 a[::-1]
MsgBox reversed.ToString()
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
8.0
8.0
[8, 27, 64]
[0, 8, 64, 216, 512]
[729, 512, 343, 216, 125, 64, 27, 8, 1, 0]Numpy.Slice(source, start, stop, step := 1, axis := 0)
对单个轴切片并返回视图 —— 不复制任何元素数据
(第 4 章)。多维访问用 ArrayGetItem 取单点、逐轴用
Slice 取范围:
; 用坐标函数构造 5x4 数组:b[x, y] = 10x + y
b := Numpy.FromFunction((x, y) => 10 * x + y, [5, 4])
MsgBox b.ToString()
point := Numpy.ArrayGetItem(b, [2, 3]) ; b[2, 3]
MsgBox point.ToArray()[1]
lastRow := Numpy.ArrayGetItem(b, [-1, 1]) ; b[-1, 1]
MsgBox lastRow.ToArray()[1]
rows := Numpy.Slice(b, 1, 3) ; b[1:3, :]
MsgBox rows.ToString()
column := Numpy.Slice(b, 1, 2, 1, 1) ; b[:, 1:2]
MsgBox column.ToString()
[[0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23],
[30, 31, 32, 33],
[40, 41, 42, 43]]
23.0
41.0
[[10, 11, 12, 13],
[20, 21, 22, 23]]
[[1],
[11],
[21],
[31],
[41]]迭代使用普通的 AHK 循环。ToArray() 给出扁平元素;
Numpy.Ndenumerate 把每个值与其坐标配对,
Numpy.Ndindex 则在没有数组的情况下遍历一个形状:
small := Numpy.Array([1, 2, 3, 4], [2, 2])
text := ""
lines := ""
for value in small.ToArray()
text .= value " "
for pair in Numpy.Ndenumerate(small)
lines .= "(" pair[1][1] ", " pair[1][2] ") -> " pair[2] "`n"
MsgBox text
MsgBox lines
1.0 2.0 3.0 4.0
(0, 0) -> 1.0
(0, 1) -> 2.0
(1, 0) -> 3.0
(1, 1) -> 4.0第 03 章形状操作
改变数组的形状
数组的形状由每个轴上的元素个数决定。可以用多种命令改变形状 —— 以下方法全部返回结果而不改动原数组:
a := Numpy.Array([2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6], [3, 4])
MsgBox a.Ravel().ToString() ; 展平(连续时为视图)
MsgBox a.Reshape([6, 2]).ToString() ; 新形状(连续时为视图)
MsgBox a.Transpose().ToString() ; 转置视图,形状 [4, 3]
MsgBox a.Flatten().ToString() ; 展平,总是副本
[2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6]
[[2, 8],
[0, 6],
[4, 5],
[1, 1],
[8, 9],
[3, 6]]
[[2, 4, 8],
[8, 5, 9],
[0, 1, 3],
[6, 1, 6]]
[2, 8, 0, 6, 4, 5, 1, 1, 8, 9, 3, 6]与 NumPy 一样,写成 -1 的维度会自动计算:
MsgBox a.Reshape([2, -1]).ToString() ; -1 -> 6
[[2, 8, 0, 6, 4, 5],
[1, 1, 8, 9, 3, 6]]不兼容的目标形状是真实的原生错误,绝不会静默回退:
source := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
try
invalid := source.Reshape([4, 2])
catch Error as err
MsgBox err.Message
NdArray.Reshape failed with status -4:
Cannot reshape array of size 6 into shape (8 elements)堆叠不同的数组
多个数组可以沿不同的轴堆叠在一起。堆叠函数接受由 NdArray 组成的 AHK Array:
a := Numpy.Array([9, 7, 5, 2], [2, 2])
b := Numpy.Array([1, 9, 5, 1], [2, 2])
MsgBox Numpy.Vstack([a, b]).ToString() ; 纵向堆叠 -> [4, 2]
MsgBox Numpy.Hstack([a, b]).ToString() ; 横向堆叠 -> [2, 4]
x := Numpy.Array([4.0, 2.0])
y := Numpy.Array([3.0, 8.0])
MsgBox Numpy.ColumnStack([x, y]).ToString() ; 一维数组作为列
[[9, 7],
[5, 2],
[1, 9],
[5, 1]]
[[9, 7, 1, 9],
[5, 2, 5, 1]]
[[4, 3],
[2, 8]]Numpy.Concatenate(arrays, axis) 是两者的一般化形式,
Numpy.Stack(arrays, axis) 沿新轴拼接。此外还有
Dstack、RowStack 以及块装配器
Numpy.Block。
把一个数组拆成几个小数组
Hsplit 沿水平轴拆分:可以传等分数量,也可以传列边界组成的
AHK Array。结果是由 NdArray 各部分组成的 AHK Array:
a := Numpy.Arange(0, 12).Reshape([2, 6])
MsgBox a.ToString()
parts := Numpy.Hsplit(a, 3) ; 三个相等的 [2, 2] 块
MsgBox parts[1].ToString() "`n---`n" parts[2].ToString()
uneven := Numpy.Hsplit(a, [3, 4]) ; 在第 3、4 列之后拆分
MsgBox uneven[1].ToString() "`n---`n" uneven[2].ToString()
. "`n---`n" uneven[3].ToString()
[[0, 1, 2, 3, 4, 5],
[6, 7, 8, 9, 10, 11]]
[[0, 1],
[6, 7]]
---
[[2, 3],
[8, 9]]
[[0, 1, 2],
[6, 7, 8]]
---
[[3],
[9]]
---
[[4, 5],
[10, 11]]Vsplit 沿垂直轴拆分,Split(source, sections,
axis) 沿任意给定轴拆分,ArraySplit 则允许不能整除该轴的
份数。
第 04 章副本与视图
对数组进行操作时,元素数据有时会复制到新数组,有时不会。与 NumPy 完全一致,共有三种情况:
完全不复制
普通的 AHK 赋值从不复制 —— 两个名字指向同一个 NdArray
对象和同一个原生所有者:
a := Numpy.Arange(0, 12).Reshape([3, 4])
b := a ; 同一对象,没有新的原生数组
MsgBox (b = a) ; 1(引用相同)
视图:看向同一份数据
View()、Slice、Transpose(),以及在
连续数组上的 Reshape/Ravel 都返回视图:新的数组对象
共享底层元素缓冲区。Numpy.SharesMemory 可以证明这一点:
a := Numpy.Arange(0, 12).Reshape([3, 4])
v := a.View()
r := a.Reshape([6, 2]) ; 视图:a 是 C 连续的
t := a.Transpose() ; 交换步长的视图
c := a.Copy() ; 深副本
MsgBox Numpy.SharesMemory(a, v) ; 1
MsgBox Numpy.SharesMemory(a, r) ; 1
MsgBox Numpy.SharesMemory(a, t) ; 1
MsgBox Numpy.SharesMemory(a, c) ; 0
; 通过基数组写入,在每个视图中都可见:
a.SetItem(0, 99)
MsgBox r.GetItem(0) ; 99.0
1
1
1
0
99.0视图在内部持有其所有者,因此先释放源数组再释放视图是安全的。 但为了代码可读,仍建议先释放派生数组、后释放源数组(第 10 章)。
深副本
Copy() 完整复制数组及其数据。NumPy 快速入门中的经典习惯 ——
复制切片以便释放巨大的中间数组 —— 用法完全相同:
huge := Numpy.Arange(0, 100000000)
head := Numpy.Slice(huge, 0, 100).Copy() ; 拥有这 100 个元素
huge := 0 ; 原生缓冲区现在可以释放
Flatten() 和 TransposeCopy() 分别是
Ravel() 与 Transpose() 的总是复制版本;
AsContiguousArray() 把任何跨步视图物化为 C 连续副本。
第 05 章广播
广播让不同形状的数组可以一起运算,遵循合格验证过的 NumPy 1.25 规则:从尾部轴向前逐一比较形状,两个维度相等或其中之一为 1 时即兼容。
source := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
offsets := Numpy.Array([10, 20, 30], [1, 3])
shifted := Numpy.Add(source, offsets)
MsgBox shifted.ToString()
; 单元素数组可以对任何形状广播 —— 这就是标量的写法:
doubled := Numpy.Multiply(source, Numpy.Full([1], 2.0))
MsgBox doubled.ToString()
; 先问再算:
MsgBox Numpy.CanBroadcast(source, offsets) ; 1
resultShape := Numpy.BroadcastShapes([[3, 1], [1, 4]])
MsgBox "[" resultShape[1] ", " resultShape[2] "]" ; [3, 4]
[[11, 22, 33],
[14, 25, 36]]
[[2, 4, 6],
[8, 10, 12]]
1
[3, 4]Numpy.BroadcastTo(source, shape) 显式物化广播视图,
Numpy.BroadcastArrays 则把一整组数组互相扩展。
第 06 章高级索引与索引技巧
cnumpy 提供 NumPy 的整套花式索引工具;选择器是显式的函数而非方括号语法。
用索引数组索引
squares := Numpy.Multiply(Numpy.Arange(0, 12), Numpy.Arange(0, 12))
MsgBox squares.ToString()
picked := Numpy.Take(squares, [1, 1, 3, 8, 5]) ; 索引可以重复
MsgBox picked.ToString()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
[1, 1, 9, 64, 25]Take 接受普通的 AHK Array、整数或 NdArray
索引;带 axis 参数时沿该轴选取。Numpy.FancyIndex(source,
indices, axis) 是跨步版本,Numpy.Put(destination, indices,
values) 则是写入方向的对应操作。
用布尔数组索引
比较运算产生 DT_BOOL 数组,配合 BooleanIndex
选取元素。非布尔掩码会被真实错误拒绝 —— 请用比较运算构造掩码:
a := Numpy.Arange(0, 12).Reshape([3, 4])
mask := Numpy.Greater(a, Numpy.Full([1], 4.0)) ; bool 数组
MsgBox mask.ToString()
MsgBox Numpy.BooleanIndex(a, mask).ToString() ; 一维选取结果
[[False, False, False, False],
[False, True, True, True],
[True, True, True, True]]
[5, 6, 7, 8, 9, 10, 11]Where 家族
; 两分支选择:where(condition, x, y)
x := Numpy.Arange(0, 6)
isBig := Numpy.GreaterEqual(x, Numpy.Full([1], 3.0))
capped := Numpy.Where(isBig, Numpy.Full([6], 3.0), x)
MsgBox capped.ToString()
; 非零元素的坐标:每个维度一个索引数组
grid := Numpy.Array([1, 0, 0, 0, 2, 0], [2, 3])
locations := Numpy.Where(Numpy.NotEqual(grid, Numpy.Zeros([1])))
MsgBox locations[1].ToString() " " locations[2].ToString()
[0, 1, 2, 3, 3, 3]
[0, 1] [0, 1]相关工具还有 ArgWhere(坐标按行排列)、
FlatNonzero、CountNonzero、Extract、
Compress,以及多条件选择的 Select。
搜索与排序
source := Numpy.IntArray([3, 1, 2, 2, 5, 4], [2, 3])
MsgBox source.Sort(-1, "stable").ToString() ; 逐行排序
MsgBox source.Sort("none", "stable").ToString() ; 展平后排序
MsgBox source.Argsort(-1, "heapsort").ToString() ; 排序索引
parts := Numpy.Unique(source, true, true, true)
MsgBox parts[1].ToString() ; 唯一值
[[1, 2, 3],
[2, 4, 5]]
[1, 2, 2, 3, 4, 5]
[[1, 2, 0],
[0, 2, 1]]
[1, 2, 3, 4, 5]Unique 可选返回首次出现的索引、逆映射和计数。集合运算
(Intersect1d、Union1d、Setdiff1d、
In1d/Isin)与搜索组合
(Searchsorted、Digitize)补全了这一族。
第 07 章线性代数
核心线性代数接口遵循 numpy.linalg。简单的数组操作在数组
实例上;求解器与分解在 Numpy 和 Numpy.Linalg 上:
a := Numpy.Array([1.0, 2.0, 3.0, 4.0], [2, 2])
MsgBox a.Transpose().ToString()
MsgBox Numpy.Linalg.Inv(a).ToString()
eye := Numpy.Eye(2) ; 2x2 单位矩阵
MsgBox Numpy.TraceExt(eye) ; 2.0(AHK 数字)
det := a.Det() ; 单元素 NdArray
MsgBox det.GetItem(0) ; -2.0000000000000004(LU 舍入,
; 与 np.linalg.det 一致)
; 求解 a @ x = y
y := Numpy.Array([5.0, 7.0], [2, 1])
x := Numpy.Solve(a, y)
MsgBox x.ToString()
[[1, 3],
[2, 4]]
[[-2, 1],
[1.5, -0.5]]
2.0
-2.0000000000000004
[[-3],
[4]]分解操作把天然的多个结果作为数组的 AHK Array 返回:
Numpy.Eig(a) 给出 [特征值, 特征向量],
Numpy.Svd(a) 给出 [U, S, Vh],
Numpy.Lstsq(a, b) 给出
[x, 残差, 秩, 奇异值]。另外还有
Cholesky、Eigh/Eigvalsh、
Pinv、MatrixPower、MatrixRank、
Norm、Cond、Kron、
Tensordot,以及 Einsum* 固定模式。
Numpy.SetNumThreads(n) 配置 GEMM 线程池
(0 恢复自动线程数)。大规模
Dot/Matmul 调用最能从中获益。
第 08 章随机数
Numpy.Random 按常见分布生成数组。注意与 NumPy 的签名差异:
形状在前,分布参数在后:
Numpy.Random.Seed(42) ; 确定性序列
u := Numpy.Random.Random([2, 3]) ; 均匀分布 [0, 1)
n := Numpy.Random.Normal([1000], 2.0, 0.5) ; 均值 2.0,标准差 0.5
i := Numpy.Random.Randint([5], 0, 10) ; [0, 10) 内的整数
MsgBox "u shape: [" u.Shape[1] ", " u.Shape[2] "]"
MsgBox "样本均值: " Format("{:.2f}", n.Mean())
deck := Numpy.Arange(0, 10)
shuffled := Numpy.Random.Permutation(deck) ; 新的重排数组
Numpy.Random.Shuffle(deck) ; 就地打乱
choice := Numpy.Random.Choice(deck, 3, false) ; 抽 3 个,不放回
u shape: [2, 3]
样本均值: 2.00 (近似值;随生成器而定)生成器采用已鉴定的 xoshiro256** / SplitMix64 序列,不是
NumPy 的位生成器。分布语义经过合格验证,但同一种子不会逐元素复现 NumPy
的随机流。Choice 支持带概率数组的加权抽样,并校验权重
构成合法的概率分布。
第 09 章回调与向量化
NumPy 传递 Python 可调用对象的地方,cnumpy 通过批量回调 ABI 把 AHK
函数送过原生边界。回调值是真实的 double 标量;回调内部抛出的
异常会原子地中止操作,并重新抛回你的脚本。
; 从坐标构造(0 起始),类似 np.fromfunction:
grid := Numpy.FromFunction((x, y) => 10 * x + y, [2, 3])
MsgBox grid.ToString()
; 逐元素应用标量函数,类似 np.vectorize:
source := Numpy.Array([1.0, 2.0, 3.0])
tripled := Numpy.Vectorize(value => value * 2 + 1, source)
MsgBox tripled.ToString()
; 沿某个轴把每条线归约为一个标量:
SumLine(values) {
total := 0.0
for value in values
total += value
return total
}
m := Numpy.Array([1, 2, 3, 4, 5, 6], [2, 3])
columnTotals := Numpy.ApplyAlongAxis(SumLine, 0, m)
MsgBox columnTotals.ToString()
; 从迭代器拉取值,类似 np.fromiter:
MakeCounter() {
i := 0
return (*) => (i += 1, i * i)
}
squares := Numpy.FromIter(MakeCounter(), 5)
MsgBox squares.ToString()
[[0, 1, 2],
[10, 11, 12]]
[3, 5, 7]
[5, 7, 9]
[1, 4, 9, 16, 25]回调批量化的是边界穿越,而不是标量计算本身:每个元素仍要执行
一次 AHK 函数调用。合格基准中回调密集型操作(如
FromFunction)的比值远高于原生内核。凡是存在内置数组操作的
地方都应优先使用;只有当逻辑确实无法用其他方式表达时才使用回调。
用一个统计常客收尾 —— Histogram 返回 int64 的分箱计数
(两个端点相等时自动检测范围):
Numpy.Random.Seed(7)
data := Numpy.Random.Normal([10000], 2.0, 0.5)
counts := Numpy.Histogram(data, 10)
MsgBox counts.ToString() ; 覆盖样本范围的 10 个箱
MsgBox counts.Sum() ; 10000.0
第 10 章原生数组转换
cnumpy 的结果通常经由外观层消费。当你需要一个真正的 AutoHotkey
Array —— 交给另一个 AHK 库、界面或嵌套值时 ——
NdArray.ToNativeArray() 无需逐元素的 AHK 循环即可转换。
解释器的 Array 布局在运行时被发现并交叉验证;DLL 通过
cnp_ahk_fill_array_flat 与
cnp_ahk_fill_array_nd 填充预构建的树。不写死任何解释器
偏移,也不嵌入机器码。
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
matrix := Numpy.Arange(0, 12).Reshape([3, 4])
native := matrix.ToNativeArray()
MsgBox native[2][3] ; 6.0
MsgBox native.Length ; 3
MsgBox Type(native) ; Array
6.0
3
Array该转换要求数值型、C 连续数组。跨步视图会抛出
ValueError;非数值 dtype 会抛出
TypeError。结果是由 AHK 所有的深拷贝,因此源数组保持可写
且相互独立。在 1,000,000 个 float64 元素上,原生路径比 AHK 循环的
ToArray() 快约 39 倍;1000x1000 矩阵约 12 ms 完成转换。
运行 benchmark\native_conversion_benchmark.ahk 可获取完整
对照表。
第 11 章所有权与清理
这是唯一没有 NumPy 对应物的一章,也是长期运行的 AutoHotkey 进程里
最重要的一章。每个 NdArray 都拥有一个原生句柄;当最后一个
AHK 引用消失时,其析构器释放原生引用。守规矩脚本的纪律:
Init()之后用Numpy.AllocatedMemory()取一条基线。- 在
try中完成工作;退出路径上把每个数组引用赋值为0,派生数组在前。 - 最后才调用
Numpy.Cleanup(),绝不在数组 或回调结果存活时调用。 - 断言保留字节数回到了基线。
#Requires AutoHotkey v2.0
#Include ahk\numpy.ahk
Numpy.DllPath := A_ScriptDir "\build\x64\Release\cnumpy_ahk.dll"
Numpy.Init()
baseline := Numpy.AllocatedMemory()
source := 0
result := 0
try {
source := Numpy.Arange(0, 1024)
result := Numpy.Sqrt(source)
MsgBox "平方根均值: " Format("{:.2f}", result.Mean())
} finally {
result := 0 ; 派生数组在前
source := 0
retained := Numpy.AllocatedMemory()
Numpy.Cleanup() ; 总是最后
}
if retained != baseline
throw Error("retained native bytes: " (retained - baseline))
平方根均值: 21.32
(脚本以零保留字节退出)AllocatedMemory() 是库跟踪的原生分配总量 —— 它是生命周期
断言,不是 Windows 进程工作集。原生失败以携带原生状态码与消息的 AHK
异常浮出水面,绝不会被转换成空数组或替代结果。常见症状:
| 症状 | 含义与处置 |
|---|---|
加载时 GetLastError 193 | 架构不匹配 —— 用 64 位 AutoHotkey 搭配 x64 DLL。 |
加载时 GetLastError 126 | 路径或原生依赖缺失 —— 核对绝对的 Numpy.DllPath。 |
missing native export … | 包装器与 DLL 来自不同的构建 —— 一起部署它们。 |
failed with status -4 | 形状契约失败 —— 完整阅读原生消息。 |
failed with status -6 | 轴对该数组秩或投影 API 无效。 |
| 保留字节非零 | 还有所有者、视图、结果或回调上下文存活 —— 释放具体引用后重新断言。 |
延伸阅读
- AutoHotkey v2 中的 cnumpy 入门 —— 第 1–2 章与第 10 章的散文式姊妹篇
- 实用的完整示例 —— CSV 销售分析、最小二乘回归、信号平滑、预分配 C 管线
- 来自 AutoHotkey 与 C 的批量回调 —— 第 9 章背后的完整回调 ABI
- NumPy 1.25 兼容性声明 —— 每一处有意为之的差异,均经验证与测试
- 仓库 README —— 构建、测试与基准说明