
简介这是一份用于海水深度计算的MATLAB脚本sw_dpth.m面向物理海洋学方向的学生与科研人员针对声纳测深、大地水准面参考基准下的深度解算问题提供可运行代码。压缩包内共1个文件为单一.m脚本大小仅1KB结构简洁适合直接调用、阅读和二次开发。脚本覆盖多条深度计算链路从声纳发射与回波信号的时间差换算深度到结合温度、盐度、压力对声速的影响进行误差校正再通过坐标转换衔接经纬度与大地坐标同时涉及大地水准面参考、地球曲率修正等数学模型并附带数据清洗、滤波等基础处理逻辑方便理解整个海洋深度测量流程。对于希望掌握MATLAB在物理海洋学中应用的初学者这份代码相当于一套浓缩的完整范例可快速上手声速剖面计算、大地水准面修正、误差估算等核心环节目前已有160人学习浏览后续也可作为开发数据可视化、NetCDF读取等扩展功能的起点。1. 为什么sw_dpth计算深度时总差一层在一次架构评审里我看到服务模块依赖图网关依赖业务层业务层依赖数据访问层数据访问层是叶子。直觉告诉我深度是 3可脚本算出来却是 2。问题不是代码错了而是深度这个词在依赖分析里可以指节点数、边数也可以指最长路径经过的包个数。sw_dpth这个工具要解决的就是这件事给计算深度一个可编程、可复现的语义再扫描代码库里的 import 关系输出每个模块的深度和全局最大深度。你不需要引入图数据库或重量级框架一个 Python 脚本就能在本地和 CI 里跑通。这篇文章会从深度定义讲到命令行实现、参数调优以及把它接进架构门禁的具体做法。适合已经会用 AST 但还没把依赖分析工具化的工程师也适合想给微服务或后端项目加一道健康检查的团队。2. 计算深度前先把“依赖深度”定义成可执行规则2.1 三选一深度是边数、节点数还是祖先数在写sw_dpth之前得先回答一个看起来多余的问题依赖深度到底从 0 开始还是从 1 开始。团队里最常见的三种定义彼此之间差一个数但表达的意义完全不同。定义叶子模块深度入口模块深度典型用途边数路径长度0N调用链跳数、依赖距离节点数路径长度1N1模块层级编号祖先节点数量0N循环依赖检测辅助这里 N 是入口到叶子经过的依赖边数。比如A - B - C边数深度中 A2、B1、C0节点数深度中 A3、B2、C1祖先数量中 A2、B1、C0。sw_dpth默认采用边数路径长度因为它在图论里对应最短路径或最长路径的步数和依赖分析工具中常见的距离概念一致。如果团队对第几层已经有一套约定也可以通过参数改成节点数语义但我不建议混用。为什么这件事容易错因为很多场景里的深度是从 0 开始的比如网络爬虫里的页面深度根 URL 是 0下一个页面是 1。而依赖深度通常把根模块放在最上面叶子模块的深度是 0。两种深度方向相反放在同一个报表里不做说明评审会上一定会有人质疑你这个深度是不是算反了。所以sw_dpth在输出里需要保留定义说明或者在表头明确标出是边数深度。2.2 把 import 关系建模成有向图依赖深度不是一个全局常量而是每个模块在图中的属性。图上的每个节点是一个模块每条有向边u - v表示u依赖v。深度depth(u)定义为从u出发沿着边走到叶子节点的最大步数。这个定义有两个性质没有出边的叶子节点depth 0。有出边的节点depth max(depth(v) for v in neighbors(u)) 1。这两个性质可以直接翻译成递归函数也是后面sw_dpth核心算法的雏形。下面是最小的 Python 表达def depth(dep_graph, node, memo): if node in memo: return memo[node] children dep_graph.get(node, []) if not children: memo[node] 0 return 0 memo[node] max(depth(dep_graph, c, memo) for c in children) 1 return memo[node] # 示例A 依赖 B, CB 依赖 DC 依赖 D g { A: [B, C], B: [D], C: [D], D: [] } memo {} print(depth(g, A, memo)) # 输出 2这段代码先查记忆化字典没命中就递归取子节点深度最大值再加 1。加 1 表示走到子节点用了那条边。memo保证每个节点只算一次时间复杂度是 O(VE)。如果图里有环这段代码会栈溢出这个问题第四章再处理。人眼数 import 行数根本替代不了这个算法。一个 500 行的.py文件可能只 import 了三个模块但每个模块又层层依赖它的深度可能比一个 import 了二十个叶子的文件还深。依赖深度要的是路径不是行号更不是 import 语句在文件里的排列位置。把这段递归函数跑在真实依赖字典上才能得到可复现的数字。2.3 计算前的三个约定起点、跨越、去向要落地成工具光有递归函数还不够。第一起点是谁入口模块通常可以指定为--start比如app或main。不指定时sw_dpth会把所有没有入边的节点当作根也就是依赖图顶端的模块。第二外部包算不算节点from flask import Flask里的flask一般不算因为它的内部深度不影响我们工程的架构。第三循环依赖怎么算我的默认做法是检测到环时把当前节点深度记成 0同时输出警告。这样会低估真实路径但不会让程序崩溃。这三个约定直接影响输出结果。同一个代码库起点不同深度不同外部包不排除深度会被第三方库内部结构拉高。sw_dpth的参数设计就应该围绕这些约定展开而不是把深度算法写死。3. 用 Python 实现sw_dpth从解析 import 到输出深度3.1 用 AST 扫描代码库生成依赖字典动手实现时第一步是把.py文件变成依赖字典。常见做法是用ast模块读取文件找到所有Import和ImportFrom节点再把模块名整理成统一的绝对包名。下面这段代码处理绝对导入和简单的相对导入import ast from pathlib import Path def module_name_from_file(file: Path, root: Path) - str: 把文件路径转成包名如 src/app/service.py - app.service rel file.relative_to(root) parts list(rel.with_suffix()) return ..join(parts) def scan_imports(py_file: Path, root: Path): tree ast.parse(py_file.read_text(encodingutf-8)) imports [] for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: imports.append(alias.name) elif isinstance(node, ast.ImportFrom) and node.level 0: imports.append(node.module) elif isinstance(node, ast.ImportFrom) and node.level 0: current_mod module_name_from_file(py_file, root).split(.) parent current_mod[:len(current_mod) - node.level 1] if node.module: full ..join(parent [node.module]) else: full ..join(parent) imports.append(full) return imports代码里的module_name_from_file把src/app/service.py转成app.service这样就能和 import 语句里的模块名对上。ast.walk会遍历 AST 所有节点包括嵌套在if分支里的 import。ImportFrom的level字段表示相对导入层级level0是绝对导入level1是当前包level2是上一级包。相对导入转换时先取当前模块的父级包前缀再拼接node.module得到全局唯一的模块名。然后用多个文件构建依赖字典。这里有一个关键动作过滤外部包。只保留本工程内部的模块路径def build_dependency_graph(src_dir: Path) - dict: dep_graph {} for py_file in src_dir.rglob(*.py): if py_file.name.startswith(_): continue name module_name_from_file(py_file, src_dir) dep_graph[name] set() for imp in scan_imports(py_file, src_dir): if imp in dep_graph or any(imp.startswith(m) for m in dep_graph): dep_graph[name].add(imp) return {k: list(v) for k, v in dep_graph.items()}set用来去重避免同一个模块被 import 多次导致重复边。过滤逻辑写得很保守只要被导入的模块名出现在dep_graph的键里就认为是内部模块。这个办法在目录结构和包名一致时够用遇到包名和目录名不一致会漏更稳的方案是把所有模块名先收集成set再在扫描时查询这个集合。3.2 深度计算主算法DFS 记忆化 环保护依赖字典造好了核心部分就是第二章的depth()。为了让sw_dpth在命令行里可复用需要把入口节点和最大深度一起输出。下面是带环保护的版本def calc_depths(dep_graph: dict, starts: list) - dict: depth {} visiting set() def visit(node): if node in depth: return depth[node] if node in visiting: print(f[warn] cycle detected at {node}) return 0 visiting.add(node) children dep_graph.get(node, []) if not children: d 0 else: d max((visit(c) for c in children if c in dep_graph), default0) 1 visiting.remove(node) depth[node] d return d for s in starts: if s in dep_graph: visit(s) return depth这个版本比第二章多了两个改动visiting集合标记当前递归路径上的节点遇到环返回 0 并警告max生成器加了default0当所有子节点都被过滤掉时当前节点深度也是 0。逻辑上visit对每个子节点求深度取最大值再加 1就是当前节点的深度。注意visiting.remove(node)必须在递归返回后执行否则会把后续路径误判成环。3.3 命令行入口与输出格式有了前两步sw_dpth已经可以做成脚本。命令行参数我一般保持最少--start指定入口--format选择文本还是 JSON。完整入口如下import argparse if __name__ __main__: parser argparse.ArgumentParser(descriptionsw_dpth 计算依赖深度) parser.add_argument(--src, defaultsrc, help源码目录) parser.add_argument(--start, nargs*, help入口模块名如 app main) parser.add_argument(--format, defaulttext, choices[text, json]) args parser.parse_args() graph build_dependency_graph(Path(args.src)) starts args.start if args.start else [m for m in graph if all(m not in deps for deps in graph.values())] result calc_depths(graph, starts) if args.format json: import json print(json.dumps(result, indent2)) else: for name, d in sorted(result.items(), keylambda x: x[1], reverseTrue): print(f{name}\t{d})--start不传时starts自动取所有没有入边的节点也就是依赖树顶端的模块。text格式按深度降序输出模块名和深度之间用制表符分隔方便终端里排序。json格式留给 CI 趋势分析。sw_dpth完整实现大约两百行但核心就是 AST 扫描、依赖字典构建、深度递归三个部分。4.sw_dpth参数调优与边界场景循环依赖、多入口与相对导入4.1 循环依赖别让递归栈变成事故现场前面calc_depths用visiting处理环返回 0。这个策略能保证程序不崩但说不上准。更好的做法是把环识别出来同时允许继续计算最长路径。我会维护一个stack列表发现当前节点已经在stack里时输出环的完整路径def calc_depths_with_cycle(dep_graph, starts): depth, stack {}, [] def visit(node): if node in depth: return depth[node] if node in stack: cycle stack[stack.index(node):] [node] print(f[cycle] { - .join(cycle)}) return 0 stack.append(node) children [c for c in dep_graph.get(node, []) if c in dep_graph] d max((visit(c) for c in children), default0) 1 if children else 0 stack.pop() depth[node] d return d for s in starts: visit(s) return depth环内节点返回 0 会让深度被低估但至少日志里能看到是哪些模块形成了环。对架构门禁来说知道有环常常比知道准确深度更有价值。如果一定要给环内节点一个合理值可以按强连通分量缩点来做但复杂度会高一个级别。大部分项目不会为了环内的准确深度上 Tarjan 算法所以我只提供--cycle-policy warn|ignore两个选项。4.2 多入口与孤立模块深度为 0 不一定是好消息不传--start时sw_dpth会把所有没有入边的模块当入口。如果一个模块既没有入边也没有出边它既是入口又是叶子深度必然为 0。但一个只有几十行的配置文件深度是 0和一个被全网依赖的工具模块深度是 0意义完全不同。孤立模块没有参与依赖传播工具模块是依赖链的尽头。我建议把孤立模块单独列出来def report_orphan(graph, depths): orphans [m for m in graph if not graph[m] and all(m not in v for v in graph.values())] for o in orphans: print(f[info] orphan module: {o})判断条件是not graph[m]表示没有出边all(m not in v for v in graph.values())表示没有入边。多入口项目会产生多个独立的深度值text格式里最好按模块分组显示而不是只给一个最大值。否则你只能看到一个模块的深度其他入口的依赖链变化被掩盖了。4.3 参数表什么时候该用哪个把sw_dpth从一次性脚本变成工程工具参数设计至关重要。以下是我习惯维护的一组参数。参数类型默认值作用--srcstrsrc代码目录--startlist自动根节点强制指定入口模块--excludelist空排除某些模块如__main__、tests--cycle-policystrwarnwarn或ignore--max-depthint无超过该值时退出码 1--formatstrtexttext或json--max-depth主要在 CI 里用本地调试时先跑一次不带阈值的版本看最大深度在哪里再决定阈值。--exclude适合排除测试目录或生成代码比如 protobuf 生成的_pb2.py这些代码不该被算进业务模块的依赖深度。4.4 性能从几百个文件到几万个文件纯 Python 的sw_dpth在几千个.py文件的项目里足够快瓶颈在 AST 解析而不是深度算法。到几万文件时ast.parse的耗时开始明显增长。常见做法是启用多进程扫描或者用compileall的字节码缓存加速。深度算法本身是 O(VE)递归深度会被 Python 默认限制如果模块链超过 1000 层需要手动sys.setrecursionlimit。另一个容易被忽略的性能坑是dep_graph里存了大量字符串内存会被模块名复制撑大。对大型 monorepo我一般先把模块名映射成整数 ID算完深度再映射回字符串能省近一半内存。5. 把sw_dpth接进 CI用深度阈值拦住架构腐化5.1 用--max-depth做一次浅层检查在 CI 里最直接的用法是设置--max-depth超过阈值就让任务失败。比如入口是app允许最大深度 6python sw_dpth.py --src src --start app --max-depth 6 --format text命令执行后如果最大深度小于等于 6退出码为 0否则退出码为 1流水线标红。不要一开始把阈值设得很小老项目会被历史债务瞬间打挂。我通常先跑一遍看当前最大深度把这个值加 1 作为初始阈值之后每个季度收紧一档。5.2 用 JSON 输出做趋势图深度不是一次性指标它会在每次新增依赖后悄悄变大。把--format json的结果落库就能观察趋势python sw_dpth.py --src src --format json depth_report_$(date %Y%m%d).json配合jq可以只看最大深度和对应模块jq -r to_entries | sort_by(-.value) | .[0] depth_report_20250101.jsonto_entries把 JSON 对象转成数组sort_by(-.value)按深度倒序排取第一个就拿到最大深度对应的模块和数值。这条命令适合写在巡检脚本里比解析 text 格式稳定。5.3 只对新增文件卡点避免老账重提最推荐的用法是只检查本次提交新引入的依赖深度。先用git diff拿到改动过的.py文件再用--start指向这些文件对应的模块名。这样老代码里的深度可以保持现状新代码一旦把依赖链拉长就会立刻触发告警。new_file_mods$(git diff --name-only --diff-filterAM HEAD~1 | grep \.py$) python sw_dpth.py --src src $(printf -- --start %s ${new_file_mods[]} | sed s#/#.#g;s/\.py//)git diff --diff-filterAM选中新增或修改的文件sed把路径中的/换成.去掉.py后缀得到模块名。相加后sw_dpth只算这些模块的深度如果新模块隐式依赖了一大串旧模块深度会立刻暴露出来。这个技法我会直接写进团队规范新代码的深度上限是 5超过就重构没有商量余地。本文还有配套的精品资源点击获取