数据结构与并发实战
并查集、跳表、链表、通道、Promise、事件循环、连接池、深合并、PHP 序列化、快照、扩展数学、货币与财务函数。这一组的共同点是全部用普通 dict / list 表示状态,没有隐藏的运行时对象——所以能打印、能存、能在测试里断言。
一个脚本跑完 12 个包
import advstruct import splstruct import channel import promise import eventloop import connpool import deepmerge import phpser import snapshot import mathx import currency import finance def on_task(arg): print(" task " + str(arg)) def sq(x): return x * x def recover(err): return "recovered:" + err def never(resolve, reject): return null def main(): var uf = uf_new(6) uf_union(uf, 0, 1) uf_union(uf, 1, 2) print(str(uf_connected(uf, 0, 2)) + " " + str(uf_connected(uf, 0, 4)) + " 组数=" + str(uf_count(uf))) var sk = sk_new() sk_insert(sk, 10, "ten") sk_insert(sk, 30, "thirty") sk_insert(sk, 20, "twenty") print(sk_search(sk, 20) + " min=" + str(sk_min(sk)) + " max=" + str(sk_max(sk))) var dll = dll_new() dll_push_back(dll, "b") dll_push_front(dll, "a") dll_push_back(dll, "c") print(json_stringify(dll_to_list(dll)) + " size=" + str(dll_size(dll)) + " pop=" + dll_pop_front(dll)) var ch = ch_new(2) print(str(ch_send(ch, "x")) + " " + str(ch_send(ch, "y")) + " " + str(ch_send(ch, "z")) + " len=" + str(ch_len(ch))) var r = ch_try_recv(ch) print(str(r["ok"]) + " " + str(r["value"])) ch_close(ch) print(str(ch_is_closed(ch))) var p = pr_resolve(21) var q = pr_then(p, sq) print(str(pr_is_fulfilled(q)) + " " + str(pr_value(q))) print(str(pr_value(pr_catch(pr_reject("boom"), recover)))) print(json_stringify(pr_value(pr_all([pr_resolve(1), pr_resolve(2)])))) print("pr_new pending=" + str(pr_is_pending(pr_new(never)))) var loop = el_new() el_add_task(loop, on_task, "A") el_add_task(loop, on_task, "B") el_add_timer(loop, on_task, 2) print("待办 " + str(el_pending_count(loop))) print("tick1=" + str(el_tick(loop)) + " 剩 " + str(el_pending_count(loop))) print("run=" + str(el_run(loop))) var pool = cp_new(2) var c1 = cp_acquire(pool) var c2 = cp_acquire(pool) var c3 = cp_acquire(pool) print(str(c1["ok"]) + " " + str(c2["ok"]) + " " + str(c3["ok"]) + " " + json_stringify(cp_stats(pool))) cp_release(pool, c1["id"]) print(str(cp_available(pool)) + " idle=" + str(cp_idle_count(pool))) var merged = dm_merge({"db": {"host": "127.0.0.1", "port": 5432}, "debug": false}, {"db": {"port": 6432}, "log": "info"}) print(json_stringify(merged)) print(str(dm_path_get(merged, "db.port"))) print(json_stringify(dm_path_set({"a": {}}, "a.b.c", 1))) var s = ps_serialize({"name": "甲", "scores": [90, 85]}) print(s) print(json_stringify(ps_unserialize(s))) var snap = snap_new("api") snap_set_stored(snap, "{\"code\":0}") print(str(snap_match(snap, "{\"code\":0}")) + " " + snap_diff(snap, "{\"code\":1}")) print(str(mx_gcd(48, 18)) + " " + str(mx_lcm(4, 6)) + " " + str(mx_is_prime(97))) print(str(mx_clamp(120, 0, 100)) + " " + str(mx_lerp(0, 10, 0.25)) + " " + str(mx_hypot(3, 4))) var c = cur_new({"USD->CNY": 7.1}) cur_add_rate(c, "CNY", "JPY", 21.3) print(str(cur_convert(c, "USD", "CNY", 100)) + " " + str(cur_convert(c, "CNY", "USD", 100))) print(cur_format(1234.5, "CNY") + " " + cur_format_signed(-12.345, "USD")) print(json_stringify(cur_list(c))) print(str(fin_pmt(0.0049, 360, 1000000))) print(str(fin_compound_interest(10000, 0.03, 10, 12))) print(str(fin_npv(0.08, [-1000, 300, 400, 500]))) print(json_stringify(fin_amortize(100000, 0.0049, 6)[0]))
true false 组数=4 twenty min=ten max=thirty ["a","b","c"] size=3 pop=a true true false len=2 true x true true 441 recovered:boom [1,2] pr_new pending=true 待办 3 task A task B tick1=2 剩 1 task null run=1 true true false {"idle":0,"active":2,"total":2,"max":2} true idle=1 {"db":{"host":"127.0.0.1","port":6432},"debug":false,"log":"info"} 6432 {"a":{"b":{"c":1}}} a:2:{s:4:"name";s:1:"甲";s:6:"scores";a:2:{i:0;i:90;i:1;i:85;}} {"name":"甲","scores":{"0":90,"1":85}} true Expected: {"code":0} Actual: {"code":1} 6 12 true 100 2.5 5.0 710.0 14.084507042253522 1234.50 CNY -12.35 USD ["USD->CNY","CNY->JPY"] -5918.573588327579 3493.535471908246 17.62942640857591 {"period":1,"payment":16953.7,"interest":490,"principal":16463.7,"balance":83536.3}
advstruct:并查集与跳表
var uf = uf_new(6) # 6 个元素,初始各自一组 uf_union(uf, 0, 1) uf_connected(uf, 0, 1) # true uf_count(uf) # 剩余组数 var sk = sk_new() sk_insert(sk, 20, "twenty") sk_search(sk, 20) # twenty(不存在返回 null) sk_min(sk) / sk_max(sk) sk_delete(sk, 20)
并查集带路径压缩,适合连通分量、朋友圈合并这类问题;跳表是有序 map,按键排序遍历比 sorted(d.keys()) 更省事。uf_new(n) 的元素是 0..n-1,要映射业务 ID 自己建一层下标表。
splstruct:双向链表与定长数组
var dll = dll_new() dll_push_front(dll, "a"); dll_push_back(dll, "c") dll_pop_front(dll) # a dll_to_list(dll) # ["b","c"] var fa = fa_new(4) # 容量 4 fa_set(fa, 0, "x") fa_get(fa, 0) # x
对应 PHP 的 SplDoublyLinkedList 与 SplFixedArray。性能上没有优势(都是纯普贤实现,底层仍是 list/dict),价值在语义:固定容量、两端插入、越界报错,比裸 list 更不容易写错。
channel:有界通道
var ch = ch_new(2) # 容量 2;0 = 无缓冲 ch_send(ch, "x") # true ch_send(ch, "z") # false(满了) ch_try_recv(ch) # {"ok": true, "value": "x"} ch_close(ch)
ch_send 满了直接返回 false。它能表达「有界队列 + 背压」,不能靠它做并行。真并行用 spawn(语言本体),跨进程用消息队列。promise:状态机,但不执行 resolver
pr_resolve(21) # 已完成 pr_then(p, sq) # 441 pr_reject("boom") → pr_catch(…, recover) pr_all([pr_resolve(1), pr_resolve(2)]) # [1,2] pr_new(fn) # 永远是 pending
pr_new(resolver) 不会调用 resolver它只是把函数存进字典,状态停在 pending——链式 then / catch 对 pending 直接原样返回。所以这个包里只有 pr_resolve / pr_reject 构造的 Promise 可用。当成「带状态的结果包装」可以,当成 JS Promise 会踩空。eventloop:同步事件循环
var loop = el_new() el_add_task(loop, fn, "A") # 立即任务,回调收到 "A" el_add_timer(loop, fn, 2) # 2 个 tick 后执行,回调收到 null el_add_timer(loop, fn, 2, "T") # 四参:回调收到 "T" el_add_repeat(loop, fn, 3, "R") # 每 3 个 tick 一次 el_tick(loop) # 跑一轮,返回本轮执行数 el_run(loop) # 跑到清空
定时器的「时间」是 tick 数,不是毫秒。它适合把「先做 A 再做 B」的流程拆成任务队列来组织(避免深递归),不适合做真实延时——要 sleep 用 sleep_ms。delay > 0 的任务每 tick 递减 1,减到 0 才执行;回调里新加的任务会保留到下一轮,不会被本轮清空。
connpool:连接池状态机
var pool = cp_new(2) var c = cp_acquire(pool) # {"ok": true, "id": 0} cp_acquire(pool) # 第二个,id=1 cp_acquire(pool) # {"ok": false, "id": -1} ← 满了 cp_release(pool, c["id"]) cp_stats(pool) # {"idle":1,"active":1,"total":2,"max":2}
只管「借还计数」,不建真连接。真连接由你按 id 存一个 dict 自己管(id → mysql 连接),借出时从池里取、归还时放回。cp_available 用于快速判断要不要等待。
deepmerge:递归合并与点路径
dm_merge({"db":{"host":"h","port":5432}}, {"db":{"port":6432}})
# {"db":{"host":"h","port":6432}} ← 只覆盖同名叶子,不整体替换
dm_path_get(d, "db.port") # 6432
dm_path_set({"a":{}}, "a.b.c", 1) # {"a":{"b":{"c":1}}}
dm_deep_copy(v) # 深拷贝(防改到原对象)配置分层叠加的标准做法:默认配置 → 环境配置 → 命令行覆盖,三次 dm_merge。注意 list 的合并语义不同:dm_merge_lists 是按下标覆盖,不是追加。
phpser:PHP 序列化格式
ps_serialize({"name":"甲","scores":[90,85]})
# a:2:{s:4:"name";s:1:"甲";s:6:"scores";a:2:{i:0;i:90;i:1;i:85;}}
ps_unserialize("i:42;") # 42a:2:{i:0;…} 这种整数键数组会变成 {"0":90,"1":85}——普贤的 dict 键只能是字符串。要按数组用,自己取 values() 再排序。另:不支持对象(O:)与引用(R:),遇到会解析失败。snapshot:快照比对
var snap = snap_new("api") snap_set_stored(snap, "{\"code\":0}") # 从文件读回的旧快照 snap_match(snap, actual) # true / false snap_diff(snap, actual) # 差异描述(人读) snap_update(snap, actual) # 更新快照值
适合「接口响应结构不能变」的回归测试:把上次的响应存成快照,每次跑完比对。snap_serialize 把值转成稳定字符串(dict 键排序),所以键顺序变化不会被判成差异。
mathx:扩展数学
mx_gcd(48, 18) # 6 mx_lcm(4, 6) # 12 mx_is_prime(97) # true mx_clamp(120, 0, 100) # 100 mx_lerp(0, 10, 0.25) # 2.5 mx_hypot(3, 4) # 5.0 mx_sinh / mx_acosh / mx_factorial / mx_fibonacci
双曲函数、取模、角度弧度、阶乘与斐波那契都有。mx_factorial 超过 20 会溢出 64 位整数,大数用 big 包。
currency:汇率换算
var c = cur_new({"USD->CNY": 7.1}) # "FROM->TO" var c2 = cur_new({"USD/CNY": 7.1}) # 斜杠、空格写法等价 var c3 = cur_new({"USD": {"CNY": 7.1}}) # 嵌套写法也认 cur_add_rate(c, "CNY", "JPY", 21.3) cur_convert(c, "USD", "CNY", 100) # 710.0 cur_convert(c, "CNY", "USD", 100) # 14.0845…(自动用倒数) cur_convert_ok(c, "USD", "JPY", 100) # Err(缺少汇率: USD->JPY) cur_format(1234.5, "CNY") # 1234.50 CNY cur_format(-12.345, "USD") # -12.35 USD(按分四舍五入)
"USD->CNY" / "USD/CNY" / "USD CNY" / {"USD": {"CNY": …}} 四种写法都收敛成同一个键,大小写不敏感。反向汇率自动取倒数,不用两边都填。要判断「是不是缺这对汇率」用 cur_convert_ok(cur_convert 缺汇率时返回 null,和金额 0 分不清)。finance:PMT / 复利 / NPV
fin_pmt(0.0049, 360, 1000000) # -5918.57(月供,负号=支出) fin_compound_interest(10000, 0.03, 10, 12) # 3493.54 fin_npv(0.08, [-1000, 300, 400, 500]) # 17.63 fin_amortize(100000, 0.0049, 6) # 摊销表
{"period":1,"payment":16953.7,"interest":490,"principal":16463.7,"balance":83536.3}:三个数同一符号(都是正数,代表本期发生的金额),interest + principal = payment,balance 逐期递减、末期抹平到 0。
单点函数 fin_pmt / fin_npv / fin_irr / fin_compound_interest 数值一直是对的;fin_pmt 返回负数沿用 Excel 的现金流口径(支出为负)。