cnumpy · AutoHotkey v2 实战手册

cnumpy
快速入门教程

在 AutoHotkey v2 中直接驱动具有 NumPy 1.25 语义的原生 x64 数组。 本教程逐章对应经典的 NumPy Quickstart,每一个示例都针对 ahk/numpy.ahk 中的 Numpy 门面重新编写。

对标验证
NumPy 1.25.0
752/752 项声明
EN 中文

第 01 章开始之前

cnumpy 是一个原生的 Windows x64 DLL,提供公开的 C ABI 以及一个 AutoHotkey v2 门面。本教程中的一切都通过仓库里的两个文件运行:

组件路径
AutoHotkey 门面ahk\numpy.ahk
合格发布 DLLbuild\x64\Release\cnumpy_ahk.dll

所有组件必须使用同一架构:Windows x64、64 位的 AutoHotkey v2 解释器(发布版以 2.1-alpha.30 x64 验证),以及 x64 版 DLL。请在第一次调用库之前设置 Numpy.DllPath —— 数组工厂方法会按需调用 Numpy.Init(),而 DLL 加载后再修改路径不会替换已加载的模块。

setup.ahkAutoHotkey v2
#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元素数据的总字节数
StridesCContiguousFContiguous内存布局

一个例子

basics_example.ahkAutoHotkey v2
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:     15

Numpy.Arange 默认产生 float64 元素,因此 Numpy.Typename(a.Dtype) 报告原生名称 double precisionShape 返回的是克隆的 AHK Array —— 修改它不会改变原生数组的形状。

创建数组

Numpy.Array 接受一个扁平的 AHK 数值数组,外加可选的行主序形状。 数据长度必须恰好等于各维大小之积 —— 不存在静默截断或补齐:

creation.ahkAutoHotkey v2
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_BOOLbool
Numpy.DT_INT32有符号 32 位整数
Numpy.DT_LONGLONG有符号 64 位整数
Numpy.DT_FLOAT3232 位浮点数
Numpy.DT_FLOAT6464 位浮点数 —— 默认值
Numpy.DT_COMPLEX128两个 64 位浮点分量

很多时候数组的元素未知而大小已知。cnumpy 提供与 NumPy 相同的占位工厂, 一律以形状为第一个参数:

placeholders.ahkAutoHotkey v2
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]
与 NumPy 同样的规则

Arange 需要浮点步长时,优先使用 Linspace:它接受元素个数而非步长,因此不受浮点步长 累积误差的影响。

打印数组

NdArray.ToString() 以嵌套方括号渲染数组:最后一个轴从左到右打印, 其余轴以换行分隔,布局与 NumPy 一致。布尔值渲染为 True/False,浮点数默认最多 8 位有效数字。

printing.ahkAutoHotkey v2
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 包装(或用可广播的单元素数组):

operations.ahkAutoHotkey v2
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),既有静态方法也有实例方法:

matmul.ahkAutoHotkey v2
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 与“最后一个轴”始终有区分):

reductions.ahkAutoHotkey v2
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]]

同样的模式覆盖 MinMaxMeanStdVarProdArgmaxArgminAnyAllMedianPercentile,以及各自忽略 NaN 的 Nan* 变体。

通用函数

常见数学函数逐元素运算并产生新数组。它们既是 Numpy.* 静态方法,也是 NdArray 实例方法:

ufuncs.ahkAutoHotkey v2
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]

可用的函数族包括三角函数(SinCosArctan2Hypot……)、指数与对数(ExpLogLog2Log1p……)、取整 (FloorCeilRintTruncAround)、比较与极值(MaximumMinimumFmax)、逻辑与位运算,以及特殊函数(ErfGammaI0……)。

支持显式目标数组的运算可以完全避免结果分配 —— 目标的形状、dtype 和布局会被校验,绝不会被替换:

into.ahkAutoHotkey v2
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 维数组。
indexing.ahkAutoHotkey v2
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 取范围:

indexing_2d.ahkAutoHotkey v2
; 用坐标函数构造 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 则在没有数组的情况下遍历一个形状:

iterate.ahkAutoHotkey v2
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 章形状操作

改变数组的形状

数组的形状由每个轴上的元素个数决定。可以用多种命令改变形状 —— 以下方法全部返回结果而不改动原数组

reshape.ahkAutoHotkey v2
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 的维度会自动计算:

reshape_auto.ahkAutoHotkey v2
MsgBox a.Reshape([2, -1]).ToString()   ; -1 -> 6
输出
[[2, 8, 0, 6, 4, 5],
 [1, 1, 8, 9, 3, 6]]

不兼容的目标形状是真实的原生错误,绝不会静默回退:

reshape_error.ahkAutoHotkey v2
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:

stacking.ahkAutoHotkey v2
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) 沿轴拼接。此外还有 DstackRowStack 以及块装配器 Numpy.Block

把一个数组拆成几个小数组

Hsplit 沿水平轴拆分:可以传等分数量,也可以传列边界组成的 AHK Array。结果是由 NdArray 各部分组成的 AHK Array:

splitting.ahkAutoHotkey v2
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 对象和同一个原生所有者:

no_copy.ahkAutoHotkey v2
a := Numpy.Arange(0, 12).Reshape([3, 4])
b := a                       ; 同一对象,没有新的原生数组
MsgBox (b = a)               ; 1(引用相同)

视图:看向同一份数据

View()SliceTranspose(),以及在 连续数组上的 Reshape/Ravel 都返回视图:新的数组对象 共享底层元素缓冲区。Numpy.SharesMemory 可以证明这一点:

views.ahkAutoHotkey v2
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 快速入门中的经典习惯 —— 复制切片以便释放巨大的中间数组 —— 用法完全相同:

deep_copy.ahkAutoHotkey v2
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 时即兼容。

123 456 形状 [2, 3] + 102030 102030 形状 [1, 3] —— 行虚拟重复 = 112233 142536 形状 [2, 3]
[1, 3] 的行沿第 0 轴虚拟重复 —— 不产生任何复制。
broadcasting.ahkAutoHotkey v2
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 的整套花式索引工具;选择器是显式的函数而非方括号语法。

用索引数组索引

take.ahkAutoHotkey v2
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 选取元素。非布尔掩码会被真实错误拒绝 —— 请用比较运算构造掩码:

boolean_mask.ahkAutoHotkey v2
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.ahkAutoHotkey v2
; 两分支选择: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(坐标按行排列)、 FlatNonzeroCountNonzeroExtractCompress,以及多条件选择的 Select

搜索与排序

sorting.ahkAutoHotkey v2
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 可选返回首次出现的索引、逆映射和计数。集合运算 (Intersect1dUnion1dSetdiff1dIn1d/Isin)与搜索组合 (SearchsortedDigitize)补全了这一族。

第 07 章线性代数

核心线性代数接口遵循 numpy.linalg。简单的数组操作在数组 实例上;求解器与分解在 NumpyNumpy.Linalg 上:

linalg.ahkAutoHotkey v2
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, 残差, 秩, 奇异值]。另外还有 CholeskyEigh/EigvalshPinvMatrixPowerMatrixRankNormCondKronTensordot,以及 Einsum* 固定模式。

性能

Numpy.SetNumThreads(n) 配置 GEMM 线程池 (0 恢复自动线程数)。大规模 Dot/Matmul 调用最能从中获益。

第 08 章随机数

Numpy.Random 按常见分布生成数组。注意与 NumPy 的签名差异: 形状在前,分布参数在后:

random.ahkAutoHotkey v2
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 标量;回调内部抛出的 异常会原子地中止操作,并重新抛回你的脚本。

callbacks.ahkAutoHotkey v2
; 从坐标构造(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 的分箱计数 (两个端点相等时自动检测范围):

histogram.ahkAutoHotkey v2
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_flatcnp_ahk_fill_array_nd 填充预构建的树。不写死任何解释器 偏移,也不嵌入机器码。

native_conversion.ahkAutoHotkey v2
#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 引用消失时,其析构器释放原生引用。守规矩脚本的纪律:

  1. Init() 之后用 Numpy.AllocatedMemory() 取一条基线。
  2. try 中完成工作;退出路径上把每个数组引用赋值为 0,派生数组在前。
  3. 最后才调用 Numpy.Cleanup(),绝不在数组 或回调结果存活时调用。
  4. 断言保留字节数回到了基线。
lifecycle.ahkAutoHotkey v2
#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 无效。
保留字节非零还有所有者、视图、结果或回调上下文存活 —— 释放具体引用后重新断言。
教程完

延伸阅读