Skip to content

重构 PatchModule:200 行五态构造器拆为命名构造器,god module 按职责分层 #159

Description

@SongshGeo

现象

abses/space/patch.py 是全包最大的模块(约 1000 行),PatchModule 一个类就有 37 个方法,混了六类职责;其中 __init__ 是一个 200 行、25 个分支的五态构造器。

证据

__init__ 按五种互斥入参分派:

入参 含义
raster_file 从栅格文件建
vector_file 从矢量文件 / GeoDataFrame 建
xda 从 xarray DataArray 建
source_layer 复制已有图层
shape + resolution 从形状与分辨率建

五个分支互不共享状态,最后都汇到同一个 RasterLayer.__init__ 调用。签名里挤了 15 个关键字参数,docstring 只好用 # Resolution-based creation parameters 这种注释来分组——这本身就是「这里应该是五个函数」的信号。

分支里还有三处逐字重复的向后兼容块:

# In 0.7.x, providing attr_name automatically applied the raster data
if attr_name is not None:
    apply_raster = True

出现在 raster / vector / xarray 三个分支里,一模一样。

类的六类职责:

职责 大致范围
A. 构造 / 栅格读入 __init__
B. mesa-geo 兼容补丁 _update_transform_initialize_cells__getattr__
C. 格子存储 / 缓存 / 索引 cells_lstmaskcellsarray_cellscoords__getitem__coord_iter
D. 栅格属性 I/O _attr_or_arraydynamic_varget_xarray_add_attribute_add_dataarrayapply_rasterget_rasterreproject
E. 空间查询 / 选择 _select_by_geometryselect/selget_neighboring_cellsget_neighboring_by_indicesindices_out_of_boundstransform_coord
F. 主体聚合统计 applycount_agentsapply_agentsapply_agents 一个方法 106 行、含 3 个嵌套闭包)

建议的切分顺序(按风险从低到高)

  1. A → abses/space/patch_factory.py:五个自由函数 from_raster_file() / from_vector() / from_xarray() / from_layer() / from_shape(),各自返回统一的 (width, height, crs, total_bounds, xda)__init__ 收缩成约 30 行的分派器。这是最干净的缝——五个分支零耦合。顺带消掉上面那三份重复的兼容块。
  2. F → 独立模块或 PatchModule.agents 访问器count_agents / apply_agents 本质上是 (module, agent_type/func) 的纯函数。(注意 apply_agents 的静默吞异常问题另见 空间热路径四处静默错误:整数栅格掩膜失效 / apply_raster 无 else / mask 缓存不失效 / apply_agents 吞异常成 NaN #153。)
  3. D → 一个 RasterAttributeStore 协作对象:这组方法只操作 self._attributes + self.array_cells,是个闭合集合。
  4. E → abses/space/selection.pyselect 已经把一半工作委托给了 cells_lst.select,等于半抽出来了。

B 和 C 建议留在 patch.py——那才是真正的 mesa-geo 集成面。

顺带需要处理的两个缺陷

绑定方法上的 lru_cache

@functools.lru_cache(maxsize=1000)
def get_neighboring_by_indices(self, indices, moore, ...):

缓存挂在上、键里含 self,会为整个进程强引用最多 1000 个 PatchModuleExperiment 重复运行下就是内存泄漏),而且把同一个可变 ActorsList 发给所有调用方。建议改用实例级缓存并返回不可变副本。

assert 做输入校验:构造器路径上有 assert isinstance(resolution, ...)assert width is None and height is None 等。python -O 下会被剥掉,公开构造器不该依赖它。改成显式 raise ValueError/TypeError

验收标准

  • PatchModule.__init__ 缩到 50 行以内。
  • 五种构造方式各有一个命名构造器和对应测试(现在 patch.py 82% 的覆盖率只由 tests/api/test_nature.py 间接支撑,没有专门的测试文件)。
  • 旧的关键字入参路径保持可用(或按弃用流程标注,见 建立弃用策略:给每条 DeprecationWarning 写明移除版本,并收敛版本号来源 + 加 dependabot #162)。
  • get_neighboring_by_indices 不再跨模型实例持有引用。
  • 构造器路径上的 assert 全部换成显式异常。
  • 改动到的文件里,注释与 docstring 统一为英文。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ♻️ refactorCode restructuring without behaviour change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions