bigarraylist 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bigarraylist/__init__.py +70 -0
- bigarraylist/_api.py +453 -0
- bigarraylist/algorithms/__init__.py +22 -0
- bigarraylist/algorithms/bucket.py +200 -0
- bigarraylist/algorithms/external_sort.py +200 -0
- bigarraylist/algorithms/parallel.py +68 -0
- bigarraylist/algorithms/sample.py +51 -0
- bigarraylist/config.py +86 -0
- bigarraylist/core/__init__.py +5 -0
- bigarraylist/core/base.py +223 -0
- bigarraylist/exceptions.py +50 -0
- bigarraylist/operators/__init__.py +14 -0
- bigarraylist/operators/crud.py +89 -0
- bigarraylist/operators/search.py +212 -0
- bigarraylist/operators/sort.py +212 -0
- bigarraylist/operators/stream.py +341 -0
- bigarraylist/utils/__init__.py +45 -0
- bigarraylist/utils/scale.py +139 -0
- bigarraylist/utils/validation.py +171 -0
- bigarraylist-0.2.0.dist-info/METADATA +158 -0
- bigarraylist-0.2.0.dist-info/RECORD +24 -0
- bigarraylist-0.2.0.dist-info/WHEEL +5 -0
- bigarraylist-0.2.0.dist-info/licenses/LICENSE +21 -0
- bigarraylist-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""持久化分桶(Hash Partitioning)+ 单桶扫描查找。
|
|
2
|
+
|
|
3
|
+
对比重构前:
|
|
4
|
+
- 重构前:bucket_find() 每次调用都重建桶(每次都付 O(n) 建桶成本,
|
|
5
|
+
多次查询没有任何加速)。
|
|
6
|
+
- 重构后:build_buckets() 返回 BucketIndex 对象,建桶一次 O(n),
|
|
7
|
+
之后任意多次 .find(target) 只扫单桶 O(n/N);同时支持跨进程复用
|
|
8
|
+
(persist_path 把桶文件保留到指定目录)。
|
|
9
|
+
|
|
10
|
+
MapReduce 核心思想落地:相同 key 一定在同一个桶 → 查找时只需扫一个桶。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import pickle
|
|
18
|
+
import re
|
|
19
|
+
import tempfile
|
|
20
|
+
from collections.abc import Callable, Generator, Iterable
|
|
21
|
+
from typing import Any, Generic, TypeVar
|
|
22
|
+
|
|
23
|
+
from ..config import BigArrayConfig
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BucketIndex(Generic[T]):
|
|
29
|
+
"""持久化分桶索引。
|
|
30
|
+
|
|
31
|
+
建桶一次后可多次 find,每次只扫目标桶。
|
|
32
|
+
支持上下文管理器:
|
|
33
|
+
with build_buckets(data, key_func) as idx:
|
|
34
|
+
for tgt in targets:
|
|
35
|
+
hits = list(idx.find(tgt))
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("__key_func", "__num_buckets", "__owns_files", "__temp_paths")
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
temp_paths: list[str],
|
|
43
|
+
num_buckets: int,
|
|
44
|
+
key_func: Callable[[T], Any],
|
|
45
|
+
owns_files: bool = True,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.__temp_paths = temp_paths
|
|
48
|
+
self.__num_buckets = num_buckets
|
|
49
|
+
self.__key_func = key_func
|
|
50
|
+
self.__owns_files = owns_files # False 时 close() 不删文件(外部持久化场景)
|
|
51
|
+
|
|
52
|
+
def __enter__(self) -> BucketIndex:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(
|
|
56
|
+
self,
|
|
57
|
+
exc_type: type[BaseException] | None,
|
|
58
|
+
exc: BaseException | None,
|
|
59
|
+
tb: Any,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.close()
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
"""清理所有桶临时文件。多次调用幂等。"""
|
|
65
|
+
if not self.__owns_files:
|
|
66
|
+
return
|
|
67
|
+
paths = self.__temp_paths
|
|
68
|
+
self.__temp_paths = []
|
|
69
|
+
for path in paths:
|
|
70
|
+
try:
|
|
71
|
+
os.unlink(path)
|
|
72
|
+
except OSError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def num_buckets(self) -> int:
|
|
77
|
+
return self.__num_buckets
|
|
78
|
+
|
|
79
|
+
def find(self, target: T) -> Generator[tuple[int, T], None, None]:
|
|
80
|
+
"""查找所有 key(item) == key(target) 的元素。
|
|
81
|
+
|
|
82
|
+
返回生成器,每项为 (原始下标, 命中元素)。
|
|
83
|
+
只扫目标桶,O(n/N) 时间;O(1) 额外内存(除目标桶文件读取外)。
|
|
84
|
+
"""
|
|
85
|
+
target_key = self.__key_func(target)
|
|
86
|
+
target_bucket = hash(target_key) % self.__num_buckets
|
|
87
|
+
|
|
88
|
+
# 在 temp_paths 中定位目标桶(可能不存在 → 该桶为空)
|
|
89
|
+
target_path = None
|
|
90
|
+
for path in self.__temp_paths:
|
|
91
|
+
if self._extract_bucket_idx(path) == target_bucket:
|
|
92
|
+
target_path = path
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if target_path is None:
|
|
96
|
+
return # 目标桶为空
|
|
97
|
+
|
|
98
|
+
with open(target_path, "rb") as f:
|
|
99
|
+
while True:
|
|
100
|
+
try:
|
|
101
|
+
i, item = pickle.load(f)
|
|
102
|
+
except EOFError:
|
|
103
|
+
break
|
|
104
|
+
if self.__key_func(item) == target_key:
|
|
105
|
+
yield (i, item)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _extract_bucket_idx(path: str) -> int:
|
|
109
|
+
"""从临时文件名 big_bucket_{idx}_XXXX.tmp 提取桶下标。"""
|
|
110
|
+
m = re.search(r"big_bucket_(\d+)_", path)
|
|
111
|
+
if m is None:
|
|
112
|
+
return -1
|
|
113
|
+
return int(m.group(1))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _choose_num_buckets(total_count: int, max_buckets: int) -> int:
|
|
117
|
+
"""自动选桶数:sqrt(n) 附近的 2 的幂,上限 max_buckets。
|
|
118
|
+
|
|
119
|
+
取 2 的幂让 hash % num_buckets 利用位运算高效;上限避免文件描述符爆掉。
|
|
120
|
+
"""
|
|
121
|
+
target_n = max(8, math.isqrt(total_count))
|
|
122
|
+
n = 1
|
|
123
|
+
while n * 2 <= target_n and n < max_buckets:
|
|
124
|
+
n *= 2
|
|
125
|
+
return n
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_buckets(
|
|
129
|
+
data: Iterable[T],
|
|
130
|
+
total_count: int,
|
|
131
|
+
key_func: Callable[[T], Any] | None = None,
|
|
132
|
+
num_buckets: int | None = None,
|
|
133
|
+
*,
|
|
134
|
+
config: BigArrayConfig | None = None,
|
|
135
|
+
) -> BucketIndex:
|
|
136
|
+
"""对数据流式建桶,返回可复用的 BucketIndex。
|
|
137
|
+
|
|
138
|
+
参数:
|
|
139
|
+
data: 数据源迭代器(建议来自 _fresh_iter)
|
|
140
|
+
total_count: 数据总条数
|
|
141
|
+
key_func: 从元素提取分桶 key;None 用元素本身
|
|
142
|
+
(必须满足 a==b → key(a)==key(b) 且 hash 兼容,否则漏命中)
|
|
143
|
+
num_buckets: 桶数;None 自动选(sqrt(n) 附近 2 的幂,上限 config.bucket_max_count)
|
|
144
|
+
config: 配置对象;None 用 BigArrayConfig.default()
|
|
145
|
+
|
|
146
|
+
返回:BucketIndex,支持多次 find(target) 和上下文管理器。
|
|
147
|
+
|
|
148
|
+
内存模型:
|
|
149
|
+
- 阶段 1(建桶):流式读,每条元素 + 下标一起 pickle.dump 到对应桶文件
|
|
150
|
+
- 内存峰值 = O(桶数) 文件句柄 + O(1) 单条元素
|
|
151
|
+
"""
|
|
152
|
+
cfg = config if config is not None else BigArrayConfig.default()
|
|
153
|
+
if key_func is None:
|
|
154
|
+
key_func = lambda x: x # noqa: E731 恒等默认值
|
|
155
|
+
|
|
156
|
+
if num_buckets is None:
|
|
157
|
+
num_buckets = _choose_num_buckets(total_count, cfg.bucket_max_count)
|
|
158
|
+
if num_buckets < 1:
|
|
159
|
+
raise ValueError(f"num_buckets 必须 >= 1,得到 {num_buckets}")
|
|
160
|
+
|
|
161
|
+
bucket_files: list[Any] = [None] * num_buckets
|
|
162
|
+
temp_paths: list[str] = []
|
|
163
|
+
try:
|
|
164
|
+
# 阶段 1:流式分桶(每条元素 + 其下标一起 pickle 落盘)
|
|
165
|
+
for i, item in enumerate(data):
|
|
166
|
+
bidx = hash(key_func(item)) % num_buckets
|
|
167
|
+
if bucket_files[bidx] is None:
|
|
168
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
169
|
+
suffix=".tmp",
|
|
170
|
+
prefix=f"big_bucket_{bidx}_",
|
|
171
|
+
delete=False,
|
|
172
|
+
)
|
|
173
|
+
temp_paths.append(tmp.name)
|
|
174
|
+
bucket_files[bidx] = tmp
|
|
175
|
+
# 逐条写 (下标, 元素)
|
|
176
|
+
pickle.dump((i, item), bucket_files[bidx])
|
|
177
|
+
|
|
178
|
+
# 关闭所有已开启的桶文件(写模式 → 准备读)
|
|
179
|
+
for f in bucket_files:
|
|
180
|
+
if f is not None:
|
|
181
|
+
f.close()
|
|
182
|
+
|
|
183
|
+
return BucketIndex(temp_paths, num_buckets, key_func, owns_files=True)
|
|
184
|
+
except BaseException:
|
|
185
|
+
# 出错时清理已开的文件和已建的临时文件
|
|
186
|
+
for f in bucket_files:
|
|
187
|
+
if f is not None:
|
|
188
|
+
try:
|
|
189
|
+
f.close()
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
for path in temp_paths:
|
|
193
|
+
try:
|
|
194
|
+
os.unlink(path)
|
|
195
|
+
except OSError:
|
|
196
|
+
pass
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = ["BucketIndex", "build_buckets"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""真正的低内存外部排序。
|
|
2
|
+
|
|
3
|
+
核心改进(对比重构前的整块版本):
|
|
4
|
+
- 重构前:阶段 2 整块 pickle.load,所有 chunk_list 同时在内存,
|
|
5
|
+
内存峰值 ≈ 数据总量,违背"外部排序"的初衷。
|
|
6
|
+
- 重构后:阶段 1 逐条 pickle.dump 到文件(每条独立可 load),
|
|
7
|
+
阶段 2 每个文件用一个 lazy 文件迭代器,每次只 load 一条。
|
|
8
|
+
内存峰值 = O(块数) 文件句柄 + O(块数) 单条元素。
|
|
9
|
+
|
|
10
|
+
模式选择(由 BigArrayConfig.external_sort_streaming_merge 控制):
|
|
11
|
+
- True → streaming(逐条 load,省内存,速度稍慢)
|
|
12
|
+
- False → block(整块 load,快 3~5 倍,内存 ≈ 数据总量;与重构前一致)
|
|
13
|
+
|
|
14
|
+
通过 lazy_file_iter / lazy_block_iter 两种文件迭代器抽象两种模式,
|
|
15
|
+
上层调用方代码完全相同。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import heapq
|
|
21
|
+
import os
|
|
22
|
+
import pickle
|
|
23
|
+
import tempfile
|
|
24
|
+
from collections import deque
|
|
25
|
+
from collections.abc import Callable, Generator, Iterable
|
|
26
|
+
from concurrent.futures import Future, ProcessPoolExecutor
|
|
27
|
+
from typing import IO, Any, TypeVar
|
|
28
|
+
|
|
29
|
+
from ..config import BigArrayConfig
|
|
30
|
+
from ..utils.scale import choose_external_sort_chunk_size
|
|
31
|
+
from .parallel import sort_chunk
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T")
|
|
34
|
+
|
|
35
|
+
# 双缓冲流水线深度:阶段 1 同时最多保持 2 个 chunk 在飞行
|
|
36
|
+
# (1 个在 worker 中排序 + 1 个在主进程读取/落盘)。
|
|
37
|
+
# 排序 N 与 "读 N+1 + 落 N-1" 并行,并行度从 1 提升到 2,
|
|
38
|
+
# 内存峰值仍为 O(chunk_size)(与串行版一致,不退化成中数据分支)。
|
|
39
|
+
_BUFFER_DEPTH = 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _streaming_file_iter(f: IO[bytes]) -> Generator[T, None, None]:
|
|
43
|
+
"""逐条 pickle.load 模式的文件迭代器。
|
|
44
|
+
|
|
45
|
+
要求文件是用"逐条 pickle.dump"写入的(每次 dump 一个元素)。
|
|
46
|
+
每次 next 调用 pickle.load(f),读到 EOF 抛 StopIteration。
|
|
47
|
+
内存:O(1) per file(同时持有的元素仅 1 个)。
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
yield pickle.load(f)
|
|
53
|
+
except EOFError:
|
|
54
|
+
return
|
|
55
|
+
except GeneratorExit:
|
|
56
|
+
# 调用方 close() 生成器时也要让上层 finally 关闭文件
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _block_file_iter(f: IO[bytes]) -> Generator[T, None, None]:
|
|
61
|
+
"""整块 pickle.load 模式的文件迭代器(向后兼容用)。
|
|
62
|
+
|
|
63
|
+
要求文件是用"整块 pickle.dump(list)"写入的(一次 dump 整个 list)。
|
|
64
|
+
一次 pickle.load 读出整块 list,然后 yield from 展开。
|
|
65
|
+
内存:O(整块大小) per file;所有文件同时存在时,总内存 ≈ 数据总量。
|
|
66
|
+
"""
|
|
67
|
+
chunk_list = pickle.load(f)
|
|
68
|
+
yield from chunk_list
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _dump_streaming(chunk: list[T], f: IO[bytes]) -> None:
|
|
72
|
+
"""逐条 pickle.dump 一个 chunk 的元素到文件 f。"""
|
|
73
|
+
for item in chunk:
|
|
74
|
+
pickle.dump(item, f)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dump_block(chunk: list[T], f: IO[bytes]) -> None:
|
|
78
|
+
"""整块 pickle.dump 一个 chunk 到文件 f(向后兼容模式)。"""
|
|
79
|
+
pickle.dump(chunk, f)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def external_sort(
|
|
83
|
+
data: Iterable[T],
|
|
84
|
+
total_count: int,
|
|
85
|
+
*,
|
|
86
|
+
config: BigArrayConfig | None = None,
|
|
87
|
+
max_workers: int | None = None,
|
|
88
|
+
key: Callable[[T], Any] | None = None,
|
|
89
|
+
) -> Generator[T, None, None]:
|
|
90
|
+
"""外部排序(External Sort):分块排序 + 落盘 + 多路归并。
|
|
91
|
+
|
|
92
|
+
参数:
|
|
93
|
+
data: 数据源迭代器(建议来自 _fresh_iter)
|
|
94
|
+
total_count: 数据总条数(用于选块大小)
|
|
95
|
+
config: 配置对象;None 用 BigArrayConfig.default()
|
|
96
|
+
max_workers: 阶段 1 排序的进程池并行度
|
|
97
|
+
key: 排序键函数(类似 ``sorted(data, key=...)``);
|
|
98
|
+
None 按元素本身排序。
|
|
99
|
+
⚠️ 多进程路径要求 key 可 pickle(模块级函数或 functools.partial,
|
|
100
|
+
lambda 在 spawn 模式下不可 pickle)
|
|
101
|
+
|
|
102
|
+
返回:生成器,按 key 的非降序逐条产出已排序元素。
|
|
103
|
+
|
|
104
|
+
内存模型(streaming 模式,默认):
|
|
105
|
+
- 阶段 1:双缓冲流水线排序 + 逐条落盘。submit 一块后立即继续读下一块,
|
|
106
|
+
飞行队列满(_BUFFER_DEPTH=2 块)时才 .result() 落盘最老的一块;
|
|
107
|
+
排序 N 与 "读 N+1 + 落 N-1" 并行。内存峰值 ≈ 2*chunk_size。
|
|
108
|
+
- 阶段 2:所有块各持一个文件句柄 + lazy 迭代器,每次 yield 一条。
|
|
109
|
+
内存峰值 = O(块数)(≈ 32)文件句柄 + O(块数) 临时元素。
|
|
110
|
+
适合"数据量远超内存"的场景。
|
|
111
|
+
|
|
112
|
+
内存模型(block 模式,external_sort_streaming_merge=False):
|
|
113
|
+
- 阶段 1:双缓冲流水线排序 + 整块落盘。比逐条快 3~5 倍。
|
|
114
|
+
- 阶段 2:所有块整块 pickle.load 到内存,heapq.merge 同时驱动所有 list。
|
|
115
|
+
内存峰值 ≈ 数据总量。仅当数据总量放得下内存时使用。
|
|
116
|
+
|
|
117
|
+
约束:
|
|
118
|
+
- 元素必须可 pickle(落盘要求)
|
|
119
|
+
- 文件描述符上限:块数受 OS 限制(Linux 默认 1024)
|
|
120
|
+
块数由 choose_external_sort_chunk_size 控制(目标 32)
|
|
121
|
+
"""
|
|
122
|
+
cfg = config if config is not None else BigArrayConfig.default()
|
|
123
|
+
streaming = cfg.external_sort_streaming_merge
|
|
124
|
+
chunk_size = choose_external_sort_chunk_size(
|
|
125
|
+
total_count,
|
|
126
|
+
target=cfg.external_sort_chunk_target,
|
|
127
|
+
min_chunk=cfg.external_sort_min_chunk,
|
|
128
|
+
max_chunk=cfg.external_sort_max_chunk,
|
|
129
|
+
)
|
|
130
|
+
_dump = _dump_streaming if streaming else _dump_block
|
|
131
|
+
_file_iter_cls = _streaming_file_iter if streaming else _block_file_iter
|
|
132
|
+
|
|
133
|
+
temp_paths: list[str] = []
|
|
134
|
+
|
|
135
|
+
def _dump_chunk(sorted_chunk: list) -> None:
|
|
136
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".tmp", prefix="big_sort_", delete=False)
|
|
137
|
+
try:
|
|
138
|
+
_dump(sorted_chunk, tmp)
|
|
139
|
+
tmp.close()
|
|
140
|
+
except Exception:
|
|
141
|
+
tmp.close()
|
|
142
|
+
try:
|
|
143
|
+
os.unlink(tmp.name)
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
raise
|
|
147
|
+
temp_paths.append(tmp.name)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# 阶段 1:双缓冲流水线排序 + 落盘
|
|
151
|
+
# 改进点(对比串行版):原版每块 submit().result() 串行,排序与 I/O 完全串行。
|
|
152
|
+
# 双缓冲:submit 一块后立即继续读下一块;飞行队列满(_BUFFER_DEPTH 块)时
|
|
153
|
+
# 才 .result() 落盘最老的一块。排序 N 与 "读 N+1 + 落 N-1" 并行,并行度 1→2。
|
|
154
|
+
# 内存:飞行队列最多 2 个 future;每个 future 的 chunk 在 worker pickled 后即可
|
|
155
|
+
# 释放,主进程峰值 ≈ 2*chunk_size(与串行版一致,不退化成中数据分支)。
|
|
156
|
+
in_flight: deque[Future[list[T]]] = deque()
|
|
157
|
+
with ProcessPoolExecutor(max_workers=max_workers) as pool:
|
|
158
|
+
chunk: list = []
|
|
159
|
+
try:
|
|
160
|
+
for item in data:
|
|
161
|
+
chunk.append(item)
|
|
162
|
+
if len(chunk) >= chunk_size:
|
|
163
|
+
in_flight.append(pool.submit(sort_chunk, (chunk, key)))
|
|
164
|
+
chunk = []
|
|
165
|
+
if len(in_flight) >= _BUFFER_DEPTH:
|
|
166
|
+
_dump_chunk(in_flight.popleft().result())
|
|
167
|
+
if chunk: # 末尾不足一块的也要排
|
|
168
|
+
in_flight.append(pool.submit(sort_chunk, (chunk, key)))
|
|
169
|
+
# 排空飞行队列
|
|
170
|
+
while in_flight:
|
|
171
|
+
_dump_chunk(in_flight.popleft().result())
|
|
172
|
+
except BaseException:
|
|
173
|
+
# 异常时取消未启动的 future;已启动的让其自然结束(pool.__exit__ 会 join)
|
|
174
|
+
for fut in in_flight:
|
|
175
|
+
if not fut.running():
|
|
176
|
+
fut.cancel()
|
|
177
|
+
raise
|
|
178
|
+
|
|
179
|
+
# 阶段 2:多路归并
|
|
180
|
+
# heapq.merge 接受多个有序迭代器,逐条比较产出
|
|
181
|
+
files: list[IO[bytes]] = []
|
|
182
|
+
try:
|
|
183
|
+
files.extend(open(path, "rb") for path in temp_paths)
|
|
184
|
+
file_iters: list[Generator[T, None, None]] = [_file_iter_cls(f) for f in files]
|
|
185
|
+
yield from heapq.merge(*file_iters, key=key) # type: ignore[arg-type]
|
|
186
|
+
finally:
|
|
187
|
+
for f in files:
|
|
188
|
+
try:
|
|
189
|
+
f.close()
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
finally:
|
|
193
|
+
for path in temp_paths:
|
|
194
|
+
try:
|
|
195
|
+
os.unlink(path)
|
|
196
|
+
except OSError:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = ["external_sort"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""模块级可 pickle 函数:供 ProcessPoolExecutor 子进程调用。
|
|
2
|
+
|
|
3
|
+
⚠️ 这些函数必须放在模块级(不能是嵌套函数/lambda),否则子进程
|
|
4
|
+
pickle 时找不到定义。本文件按职责拆分原 core.py 顶部的四个函数。
|
|
5
|
+
|
|
6
|
+
子进程通过 `from bigarraylist.algorithms.parallel import _sort_chunk` 路径找到定义。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import bisect as _bisect_mod
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def linear_find_in_chunk(args: tuple[list[T], T, int]) -> list[tuple[int, T]]:
|
|
18
|
+
"""在单个 chunk 内查找所有等于 target 的元素,返回 (全局下标, 元素) 列表。"""
|
|
19
|
+
chunk, target, start_index = args
|
|
20
|
+
return [(i, item) for i, item in enumerate(chunk, start=start_index) if item == target]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sort_chunk(args: tuple[Any, ...]) -> list[T]:
|
|
24
|
+
"""对单个 chunk 排序,返回新列表(不修改原 chunk)。
|
|
25
|
+
|
|
26
|
+
args 形如 ``(chunk, key)``;``key=None`` 表示按元素本身排序。
|
|
27
|
+
向后兼容:也接受旧式 ``(chunk,)`` 一元组(等价于 ``key=None``)。
|
|
28
|
+
"""
|
|
29
|
+
if len(args) == 2:
|
|
30
|
+
chunk, key = args
|
|
31
|
+
else:
|
|
32
|
+
# 旧式 (chunk,) 调用(_sort_chunk 别名的遗留调用方)
|
|
33
|
+
(chunk,) = args
|
|
34
|
+
key = None
|
|
35
|
+
return sorted(chunk, key=key)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def chunk_is_sorted(args: tuple[list[T]]) -> bool:
|
|
39
|
+
"""判断单个 chunk 是否升序(模块级,供子进程 pickle 调用)。"""
|
|
40
|
+
(chunk,) = args
|
|
41
|
+
return all(chunk[i] <= chunk[i + 1] for i in range(len(chunk) - 1)) # type: ignore[operator]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def bisect_find_in_chunk(args: tuple[list[T], T, int]) -> list[tuple[int, T]]:
|
|
45
|
+
"""在**已排序**的单个 chunk 内用 bisect 找所有等于 target 的元素。
|
|
46
|
+
|
|
47
|
+
返回 (全局下标, 元素) 列表。
|
|
48
|
+
"""
|
|
49
|
+
chunk, target, start_index = args
|
|
50
|
+
left = _bisect_mod.bisect_left(chunk, target) # type: ignore[call-overload]
|
|
51
|
+
right = _bisect_mod.bisect_right(chunk, target) # type: ignore[call-overload]
|
|
52
|
+
return [(start_index + i, chunk[i]) for i in range(left, right)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ==================== 向后兼容别名 ====================
|
|
56
|
+
# 重构前这些函数在 bigarraylist.core 模块下,名字带下划线前缀。
|
|
57
|
+
# 现有测试 / 外部代码若直接 import,保留别名避免破坏。
|
|
58
|
+
_linear_find_in_chunk: Any = linear_find_in_chunk
|
|
59
|
+
_sort_chunk: Any = sort_chunk
|
|
60
|
+
_chunk_is_sorted: Any = chunk_is_sorted
|
|
61
|
+
_bisect_find_in_chunk: Any = bisect_find_in_chunk
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"bisect_find_in_chunk",
|
|
65
|
+
"chunk_is_sorted",
|
|
66
|
+
"linear_find_in_chunk",
|
|
67
|
+
"sort_chunk",
|
|
68
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""蓄水池抽样(Reservoir Sampling):从总量未知的流里等概率抽 k 个。
|
|
2
|
+
|
|
3
|
+
数学保证:每个元素被选中的概率都是 k/N(N 为实际总量,无需预先知道)。
|
|
4
|
+
内存 O(k),无需预知 N。大数据分析里做随机采样的必备算法。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
from collections.abc import Generator, Iterable
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reservoir_sample(
|
|
17
|
+
stream: Iterable[T],
|
|
18
|
+
k: int,
|
|
19
|
+
seed: int | None = None,
|
|
20
|
+
) -> Generator[T, None, None]:
|
|
21
|
+
"""蓄水池抽样算法 R。
|
|
22
|
+
|
|
23
|
+
参数:
|
|
24
|
+
stream: 任意可迭代对象(不要求预知长度)
|
|
25
|
+
k: 要抽取的样本数(必须 >= 1,调用方校验)
|
|
26
|
+
seed: 随机种子,None 表示系统随机;传 int 可复现
|
|
27
|
+
|
|
28
|
+
返回:生成器,产出 k 个元素(若流总量 < k,产出全部元素)
|
|
29
|
+
|
|
30
|
+
算法:
|
|
31
|
+
- 先存前 k 个到 reservoir
|
|
32
|
+
- 第 i 个(i >= k,从 0 计数即 i+1 个元素)以 k/(i+1) 概率
|
|
33
|
+
随机替换池中某一个
|
|
34
|
+
- 一遍流式扫描结束,池中 k 个即为等概率样本
|
|
35
|
+
"""
|
|
36
|
+
rng = random.Random(seed)
|
|
37
|
+
reservoir: list[T] = []
|
|
38
|
+
for i, item in enumerate(stream):
|
|
39
|
+
if i < k:
|
|
40
|
+
reservoir.append(item)
|
|
41
|
+
else:
|
|
42
|
+
# 第 i+1 个元素(索引 i >= k),以 k/(i+1) 概率替换池中随机一个
|
|
43
|
+
# randint(0, i) 返回 [0, i] 闭区间,共 i+1 个值
|
|
44
|
+
# 落在 [0, k-1] 的概率 = k/(i+1),正是我们要的
|
|
45
|
+
j = rng.randint(0, i)
|
|
46
|
+
if j < k:
|
|
47
|
+
reservoir[j] = item
|
|
48
|
+
yield from reservoir
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["reservoir_sample"]
|
bigarraylist/config.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""配置对象:所有阈值集中可配。
|
|
2
|
+
|
|
3
|
+
向后兼容:所有字段都有默认值,等于重构前的硬编码常量。
|
|
4
|
+
不传 config 时,行为与重构前完全一致。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
cfg = BigArrayConfig(linear_parallel_threshold=50_000)
|
|
8
|
+
arr = BigArrayList(data, config=cfg)
|
|
9
|
+
|
|
10
|
+
也可类级别覆盖默认值,影响所有后续实例:
|
|
11
|
+
BigArrayConfig.set_default(external_sort_chunk_target=64)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class BigArrayConfig:
|
|
22
|
+
"""全部阈值配置。
|
|
23
|
+
|
|
24
|
+
所有字段单位均为"元素条数"。各字段的工程意义见注释。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# ===== 并行分流阈值 =====
|
|
28
|
+
# 数据量 >= 该值才走多进程路径;小数据单进程更快
|
|
29
|
+
linear_parallel_threshold: int = 100_000
|
|
30
|
+
|
|
31
|
+
# ===== 内存排序分块 =====
|
|
32
|
+
# 内存多进程排序的单块大小映射表(数据量 -> 块大小)
|
|
33
|
+
# 100k→100/块、1M→300/块、10M→800/块、100M→1000/块
|
|
34
|
+
chunk_size_map: dict[int, int] | None = None # None → 用默认 _DEFAULT_CHUNK_MAP
|
|
35
|
+
|
|
36
|
+
# ===== 外部排序 =====
|
|
37
|
+
# 目标块数:32 路归并,heapq.merge 效率最高
|
|
38
|
+
external_sort_chunk_target: int = 32
|
|
39
|
+
# 每块至少 10 万条(再小进程调度不划算)
|
|
40
|
+
external_sort_min_chunk: int = 100_000
|
|
41
|
+
# 每块最多 500 万条(sorted() 单块内存安全)
|
|
42
|
+
external_sort_max_chunk: int = 5_000_000
|
|
43
|
+
# 数据量 >= 该值才走外部排序(否则走内存多进程分块)
|
|
44
|
+
external_sort_threshold: int = 1_000_000
|
|
45
|
+
# 真正的低内存归并:逐条 load 而非整块 load。
|
|
46
|
+
# True → 内存峰值 O(块数),速度较慢但能处理远超内存的数据
|
|
47
|
+
# False → 整块 load,归并内存峰值 ≈ 数据总量,速度快 3~5 倍
|
|
48
|
+
# True(符合"外部排序"语义);要恢复旧行为显式设 False
|
|
49
|
+
external_sort_streaming_merge: bool = True
|
|
50
|
+
|
|
51
|
+
# ===== 分桶查找 =====
|
|
52
|
+
# 桶数上限(避免 OS 文件描述符耗尽,Linux 默认 1024)
|
|
53
|
+
bucket_max_count: int = 1024
|
|
54
|
+
|
|
55
|
+
# ===== 默认配置管理 =====
|
|
56
|
+
_default: BigArrayConfig | None = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def default(cls) -> BigArrayConfig:
|
|
60
|
+
"""当前生效的默认配置(实例级覆盖优先于类级默认)。"""
|
|
61
|
+
if cls._default is not None:
|
|
62
|
+
return cls._default
|
|
63
|
+
return cls()
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def set_default(cls, **kwargs: Any) -> BigArrayConfig:
|
|
67
|
+
"""类级别覆盖默认值;影响后续所有未显式传 config 的实例。
|
|
68
|
+
|
|
69
|
+
kwargs 不全传时,缺失字段保留原默认值。
|
|
70
|
+
"""
|
|
71
|
+
cls._default = cls(**kwargs)
|
|
72
|
+
return cls._default
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def reset_default(cls) -> None:
|
|
76
|
+
"""恢复类级默认值为内置默认。"""
|
|
77
|
+
cls._default = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# 内存排序分块大小默认映射(不变于重构前)
|
|
81
|
+
_DEFAULT_CHUNK_MAP: dict[int, int] = {
|
|
82
|
+
100_000: 100, # 十万级 → 100 条/块
|
|
83
|
+
1_000_000: 300, # 百万级 → 300 条/块
|
|
84
|
+
10_000_000: 800, # 千万级 → 800 条/块
|
|
85
|
+
100_000_000: 1_000, # 亿级 → 1000 条/块
|
|
86
|
+
}
|