三个同源问题:内部契约(签名、继承、分派)在不同实现之间各写各的。
1. create_module 有四套互不兼容的签名
| 位置 |
签名 |
返回 |
core/protocols.py |
(name: str, *args, **kwargs) |
ModuleProtocol |
core/base_subsystem.py(@abstractmethod) |
(module_cls: Type[BaseModule], *args, **kwargs) |
Any |
space/nature.py |
(*args, module_cls=PatchModule, major_layer=False, write_crs=False, **kwargs) |
PatchModule |
human/human.py |
(name=None, *, module_cls=HumanModule, **kwargs) |
BaseModule |
同一句 subsystem.create_module(SomeCls):
- 在
BaseHuman 上,SomeCls 绑到 name;
- 在
BaseNature 上,落进 *args;
行为完全不同,而且协议里声明的那一套没有任何实现匹配。这是"看起来有协议约束、实际没有"的典型。
建议:定一个签名(推荐 create_module(module_cls, *, name=None, **kwargs)),协议、抽象方法、两个实现全部对齐;子类特有的参数(major_layer / write_crs)保留为关键字。旧调用形式按弃用流程处理。
2. Protocol 被当成真实基类继承
core/protocols.py 里的 Protocol 出现在 5 处 MRO 里:
class Actor(mg.GeoAgent, _LinkNodeActor, BaseModelElement, ActorProtocol): ...
class BaseNature(BaseSubSystem, GeoSpace, NatureSystemProtocol): ...
class BaseHuman(BaseSubSystem, _LinkContainer, HumanSystemProtocol): ...
class BaseStateManager(ABC, StateManagerProtocol): ...
class BaseObservable(ABC, Observable): ...
Protocol 本该是零成本的结构化类型标注;一旦继承,它就变成了真实的 MRO 约束。最近一次提交 2c172159 "Swap ActorsList base order to fix mesa 3.5 MRO error" 就是这个设计的直接症状——上游改一次基类顺序,这里就得跟着调。
同一文件的其他误用:
container.py:agent_cls: Type[ActorProtocol] = ActorProtocol —— 把 Protocol 当运行时默认值,真去实例化会失败。
container.py:assert isinstance(agent, ActorProtocol) —— runtime_checkable 的 Protocol 只做结构性检查,任何长得像的对象都能通过,这个断言近乎空转(何况 assert 在 python -O 下会被剥掉)。
MainModelProtocol 里声明了带默认值的可变类属性——Protocol 不是干这个的。
建议:Protocol 只用于类型标注(if TYPE_CHECKING 下的注解、函数签名),不进任何 class X(...) 的基类列表。运行时需要的共享行为改用 ABC 或 mixin。这一步能显著降低未来 mesa 升级时的 MRO 风险。
3. human/links.py:三处方向分派逐字重复 + 为 Mock 写的静默兜底
同一段 in / out / None / else 阶梯在 owns_links、clean_links_of、linked 里出现三次,连错误信息都写了三个不同版本。_LinkNodeCell._redirect 与 _LinkNodeActor._redirect 函数体完全相同,只差一个字符串参数("agents" vs "at"),_target_is_me 同理。
更严重的是三处为测试替身让路的兜底,注释写得很直白:
# 如果是Mock对象或其他不可迭代对象,返回空列表
if hasattr(agents, "__iter__"): ...
else: ...
# 如果owning返回的不是可迭代对象,则假设没有链接
except TypeError:
has_in = False
has_out = False
if not isinstance(result, tuple):
return (False, False)
这些会把真实的 TypeError 吞成「没有链接」——返回一个错误答案而不是抛异常。生产代码不该为 unittest.mock 的形状让步;正确做法是测试里用真实对象或专门的 fake。
顺带:_LinkProxy.has 在 node is None 分支返回 (has_out, has_in),而调用方 unlink 按 has_in, has_out = self.has(...) 解包——顺序是反的,值得一并核对。
建议:抽出一个 _pick_direction(direction) helper(统一错误信息),_redirect / _target_is_me 参数化掉那个字符串,删掉三处 Mock 兜底并相应修测试。
验收标准
三个同源问题:内部契约(签名、继承、分派)在不同实现之间各写各的。
1.
create_module有四套互不兼容的签名core/protocols.py(name: str, *args, **kwargs)ModuleProtocolcore/base_subsystem.py(@abstractmethod)(module_cls: Type[BaseModule], *args, **kwargs)Anyspace/nature.py(*args, module_cls=PatchModule, major_layer=False, write_crs=False, **kwargs)PatchModulehuman/human.py(name=None, *, module_cls=HumanModule, **kwargs)BaseModule同一句
subsystem.create_module(SomeCls):BaseHuman上,SomeCls绑到name;BaseNature上,落进*args;行为完全不同,而且协议里声明的那一套没有任何实现匹配。这是"看起来有协议约束、实际没有"的典型。
建议:定一个签名(推荐
create_module(module_cls, *, name=None, **kwargs)),协议、抽象方法、两个实现全部对齐;子类特有的参数(major_layer/write_crs)保留为关键字。旧调用形式按弃用流程处理。2. Protocol 被当成真实基类继承
core/protocols.py里的 Protocol 出现在 5 处 MRO 里:Protocol 本该是零成本的结构化类型标注;一旦继承,它就变成了真实的 MRO 约束。最近一次提交
2c172159 "Swap ActorsList base order to fix mesa 3.5 MRO error"就是这个设计的直接症状——上游改一次基类顺序,这里就得跟着调。同一文件的其他误用:
container.py:agent_cls: Type[ActorProtocol] = ActorProtocol—— 把 Protocol 当运行时默认值,真去实例化会失败。container.py:assert isinstance(agent, ActorProtocol)——runtime_checkable的 Protocol 只做结构性检查,任何长得像的对象都能通过,这个断言近乎空转(何况assert在python -O下会被剥掉)。MainModelProtocol里声明了带默认值的可变类属性——Protocol 不是干这个的。建议:Protocol 只用于类型标注(
if TYPE_CHECKING下的注解、函数签名),不进任何class X(...)的基类列表。运行时需要的共享行为改用 ABC 或 mixin。这一步能显著降低未来 mesa 升级时的 MRO 风险。3.
human/links.py:三处方向分派逐字重复 + 为 Mock 写的静默兜底同一段
in / out / None / else阶梯在owns_links、clean_links_of、linked里出现三次,连错误信息都写了三个不同版本。_LinkNodeCell._redirect与_LinkNodeActor._redirect函数体完全相同,只差一个字符串参数("agents"vs"at"),_target_is_me同理。更严重的是三处为测试替身让路的兜底,注释写得很直白:
这些会把真实的
TypeError吞成「没有链接」——返回一个错误答案而不是抛异常。生产代码不该为unittest.mock的形状让步;正确做法是测试里用真实对象或专门的 fake。顺带:
_LinkProxy.has在node is None分支返回(has_out, has_in),而调用方unlink按has_in, has_out = self.has(...)解包——顺序是反的,值得一并核对。建议:抽出一个
_pick_direction(direction)helper(统一错误信息),_redirect/_target_is_me参数化掉那个字符串,删掉三处 Mock 兜底并相应修测试。验收标准
create_module在协议、抽象方法、BaseNature、BaseHuman四处签名一致,并有一个跨子系统的参数化测试。core/protocols.py里的 Protocol 不再出现在任何类的基类列表中;isinstance(x, SomeProtocol)不再用于校验。links.py的方向分派只有一处实现,错误信息统一。links.py里不再有针对 Mock 对象的分支;相关测试改用真实对象。_LinkProxy.has的返回顺序与所有调用方一致,并有测试固定。