简介:这份压缩包为 NetworkX 1.11 版本的完整源码分发包,面向学习图论、复杂网络分析与社交网络建模的 Python 开发者,也适合需要在项目中实现最短路径、连通分量、中心性、图生成与读写等功能的读者。包内共 594 个文件,以 429 个 Python 源码文件为主体,辅以 117 个 rst 文档、若干 txt 说明、bz2 与 gz 压缩的示例图数据、png 图示及 css/html 等辅助文件,整体约 1.47MB,目录结构保留了库源码、文档与测试样例的层次。已有 83 人学习下载,说明该版本在旧项目维护与算法教学中仍有一定参考价值。读者可从中获得可直接阅读和复用的图算法实现、图数据读写与序列化示例、配套文档和测试用例,便于理解 NetworkX 的内部组织方式,并借助压缩包内的示例图数据开展实验、对照算法结果或排查兼容性问题。
1. networkx-1.11.zip 不是旧版本:三类人还得回头找它
前阵子有人拿着跑不起来的脚本找我:一份早年做的复杂网络实验代码,满是G.node[u]['type']这种老写法,换到新环境直接AttributeError。查下去,对方机器上装的是 3.x。把 networkx-1.11.zip 解压挂上去,二十几行报错当场消失。这类事我一年能碰上好几回:复现早年论文里的图算法实验、维护只跑在 Python 2.7 里的老脚本、给不能再改的第三方工具补一段图计算。1.11 是 1.x 系列的收尾版本,纯 Python、零必装依赖、API 还是老一套G.node/G.edge,正好落在这些代码最适应的范围内。新项目别拿它开局,但「必须原样跑通」的场景,它是最省事的一把钥匙。
2. 从 zip 到 import:三条安装路径与一次自检
拿到一个十年前的源码包,最容易出问题的地方其实在于不确定装进去的到底是不是这个版本,步骤本身反而简单。同一个环境里可能同时存在系统级的 networkx、conda 环境里的 networkx、以及PYTHONPATH里被别的脚本临时挂上去的目录,import的时候谁先命中全靠路径顺序。所以这一章的顺序是:先看清包里有什么,再选一条安装路径,最后用一段自检脚本把「我装的确实是 1.11」这件事确认下来,别急着写业务代码。
2.1 包内结构与确认版本的方法
这个 zip 是标准的源码分发包(sdist),解压后顶层一般是一个networkx-1.11/目录,里面是setup.py加源码树。没有pyproject.toml,也没有预编译的.so或.pyd,这意味着它不挑平台,Windows、Linux、老式嵌入式 Linux 都能用,只要有 Python 解释器。
| 路径 | 作用 | 用的时候注意什么 |
|---|---|---|
setup.py/setup.cfg | distutils 打包入口,版本号写在里面 | 装之前先打开确认version='1.11' |
networkx/classes/ | Graph、DiGraph、MultiGraph、MultiDiGraph四个核心类 | 所有算法都建立在这四个类上,选错类型后面全错 |
networkx/algorithms/ | 最短路、连通性、中心性、聚类、生成树、匹配等 | 大部分纯 Python 实现,不依赖 numpy |
networkx/readwrite/ | edgelist、adjlist、gml、graphml、gexf、pajek、json_graph、pickle | 各格式的可选依赖不一样,见 3.3 的表 |
networkx/drawing/ | matplotlib 后端,nx.draw系列 | 必须装 matplotlib,且要注意后端 |
networkx/generators/ | 随机图、规则图、小世界、无标度图 | 造测试数据很方便,nx.gnp_random_graph之类 |
networkx/linalg/ | 图谱、拉普拉斯矩阵、代数连通度 | 这部分才需要 numpy/scipy |
doc/、examples/ | 文档源文件和示例脚本 | 示例脚本本身就是最好的 API 对照材料 |
纯 Python 这件事值得多说一句。它的好处是没有编译环节,setup.py不会因为缺编译器、缺头文件而中断;坏处是没有加速,几千个节点跑介数中心性会很慢。所以如果你的图规模在几万节点以上,1.11 只适合做流程验证,真正的批量计算得另想办法。
提示:
networkx/这个子目录不是包根。往PYTHONPATH里挂的应该是包含networkx/的那一层,也就是有setup.py的networkx-1.11/目录。挂错一层,import networkx会报ModuleNotFoundError。
2.2 三条安装路径:setup.py、pip 装 zip、PYTHONPATH
先建隔离环境。老包和老解释器要绑在一起,最忌讳的就是图省事装进系统 Python。
# 用一个和老代码年代接近的解释器,3.8/3.9 兼容性最好 python3.9 -m venv /opt/venvs/nx111 source /opt/venvs/nx111/bin/activate # 老 sdist 走的是 setup.py,构建环境里必须有 setuptools 和 wheel python -m pip install -U pip setuptools wheel这段命令做三件事:建一个独立 venv、激活它、把构建工具升到能处理老 sdist 的版本。解释器版本这里选 3.9,是因为 3.10 起collections下的一批抽象基类被移除,3.11 又把inspect.getargspec删了,1.11 的代码在这两个版本上很容易直接导入失败。如果你手上只有 3.11/3.12,先看第 5 章和第 6 章,用兼容垫片绕过。
第一条路径,解压后本地安装,最贴近当年的使用方式:
unzip networkx-1.11.zip -d /opt/legacy/ cd /opt/legacy/networkx-1.11 # --no-build-isolation 让 pip 用当前环境里的 setuptools,而不是另建一个空构建环境 python -m pip install . --no-build-isolation--no-build-isolation是关键参数。新版 pip 默认给每个源码包建一个干净的构建环境,里面只有 pip 自己拉的最小依赖;老包没有声明构建依赖,就会在setup.py阶段报ModuleNotFoundError: No module named 'setuptools'之类的错。关掉隔离、直接用当前环境里的 setuptools,成功率最高。装完pip show networkx应该看到Version: 1.11。
第二条路径,直接让 pip 吃 zip:
python -m pip install /path/to/networkx-1.11.zip --no-build-isolation效果和上一条一样,省掉手工解压。区别在于你对解压目录没有控制权,出问题时不好翻源码。要读代码、要对齐老论文里的实现细节,我还是建议先解压。
第三条路径,什么都不装,直接挂路径:
# 只影响当前 shell,退出即失效 export PYTHONPATH=/opt/legacy/networkx-1.11:$PYTHONPATH python -c "import networkx; print(networkx.__file__)"这条最适合两种情况:一是机器上不允许改site-packages(很多产线环境就是这样);二是你需要在同一个 shell 里来回切换 1.11 和 3.x 做行为对比。缺点是没有元数据,pip list里看不到它,接手的人容易一脸茫然,所以一定要在启动脚本里写清楚。
2.3 自检脚本:三行确认版本,五行确认 API
装完不要直接上业务代码,先跑这段:
import networkx as nx # 1) 版本和物理路径:路径比版本号更能说明问题 print(nx.__version__) # 期望输出 1.11 print(nx.__file__) # 确认指向你刚装/刚挂的那个目录 # 2) 图类型与老式属性访问 G = nx.Graph() G.add_edge('a', 'b', weight=2) G.add_edge('b', 'c', weight=1) print(G.node['a']) # 1.x 写法,输出 {} print(G.edge['a']['b']) # 1.x 写法,输出 {'weight': 2} # 3) 算法冒烟 print(nx.shortest_path(G, 'a', 'c')) # ['a', 'b', 'c'] print(nx.shortest_path_length(G, 'a', 'c', weight='weight')) # 3三个检查点各有分工。第一组确认「装的是谁」,__version__可能被篡改或被别的包伪装,__file__骗不了人;如果它指向site-packages/networkx/__init__.py而不是你解压的目录,说明环境里还有一份更新的副本抢在前面。第二组确认「API 是老的」,G.node和G.edge在 2.x 之后被改名成G.nodes/G.edges,这两行能过,基本可以判定是老版本。第三组确认「算法可用且权重生效」,weight='weight'不写的话,shortest_path_length返回的是跳数 2 而不是加权距离 3,很多「结果对不上论文」的问题就出在这个默认参数上。
3. 1.11 的 API 边界:G.node、G.edge 和四个改名点
1.x 和 2.x/3.x 之间的差异不是「多了几个函数」,而是一批属性容器和返回值类型被重新设计了。这些改动对使用者是好事,对老代码是灾难。这一章把最容易踩的几处摊开讲:属性容器怎么访问、返回值是什么类型、读写格式各自要什么依赖、以及从新版本回移到 1.11 时该改哪几行。搞清楚这些边界,你在 1.11 上写代码就不会下意识地用新写法。
3.1 属性容器:G.node / G.edge 与 G.nodes / G.edges
这是最集中的出错点。1.11 里G.node是一个以节点为键的字典,G.edge是「字典的字典」;G.nodes()和G.edges()是方法,必须带括号调用,返回列表。2.x 之后G.nodes变成了视图对象,可以直接迭代,不用加括号。
import networkx as nx G = nx.Graph() G.add_edge('a', 'b', weight=2, label='link-1') G.add_edge('b', 'c', weight=1) # 1.11 正确写法:属性走 G.node / G.edge G.node['a']['color'] = 'red' G.edge['a']['b']['weight'] = 3 # 遍历节点属性:nodes 必须带括号 for n, data in G.nodes(data=True): data['degree'] = G.degree(n) # 遍历边属性:edges 必须带括号 for u, v, data in G.edges(data=True): data['weight'] = float(data.get('weight', 1.0))逻辑上要理解一件事:G.node[u]拿到的是节点属性字典的引用,你往里面塞东西,图本身立刻变;G.edge[u][v]同理。这种设计省内存,但也很危险——两个函数共享同一个图对象时,A 改了属性 B 会看见。要隔离就G.copy(),深度复制属性用copy.deepcopy。
参数上,G.nodes(data=True)的data=True决定返回的是(节点, 属性字典)二元组还是纯节点;G.edges(data=True)对应返回三元组。这两个开关在 1.11 和 2.x 里名字一样,行为也一样,是少数不用改的地方。
对比一下 2.x/3.x 的写法,方便你判断手上的代码属于哪一代:
# 2.x / 3.x 写法,在 1.11 上会报错 # G.nodes['a']['color'] = 'red' # TypeError: 'method' object is not subscriptable # for n in G.nodes: # TypeError: 'method' object is not iterable # G.edges['a', 'b']['weight'] = 3 # TypeError血泪经验是:报错信息里出现'method' object is not subscriptable,八成就是把 2.x 写法用在了 1.11 上,反之出现AttributeError: 'Graph' object has no attribute 'node',就是把 1.11 写法用在了 3.x 上。看到这两句,先别改业务逻辑,先确认版本。
3.2 返回值类型:列表、字典与生成器
第二个隐形陷阱是返回值类型。同样的函数名,1.11 返回列表或字典,2.x 返回迭代器或视图,len()、下标、二次遍历的行为都不一样。
import networkx as nx G = nx.Graph() G.add_edges_from([('a', 'b'), ('b', 'c'), ('x', 'y')]) # 连通分量:不同小版本返回值有差异,统一 list() 包一层最稳 comps = list(nx.connected_components(G)) print(len(comps), [len(c) for c in comps]) # 度:1.11 里 G.degree() 返回 {节点: 度} 的字典,G.degree(n) 返回整数 deg_all = dict(G.degree()) deg_a = G.degree('a') # 1.x 专有的迭代器版本,2.x 已移除 for n in G.nodes_iter(): pass for u, v in G.edges_iter(): passlist(nx.connected_components(G))这一层包装看着多余,实际上是保命的。1.11 到 2.x 之间这个函数的返回类型变过,直接len()或直接遍历两遍,在不同小版本上会时对时错,而这类错误往往在换机器复现时才暴露。养成「拿不准就list()转一次」的习惯,代价很小。
G.nodes_iter()和G.edges_iter()是 1.x 的产物,作用是遍历时不复制整个列表,省内存。2.x 把这些函数删了,因为视图对象本身就够省。你在 1.11 上写的代码如果用了这两个,将来要升级就得全量替换,所以我的建议是:新写的代码直接用G.nodes(data=True),老代码保持原样不动,别为了「看起来新」去改。
参数层面还有一个容易忽略的点:G.degree(weight='weight')算的是加权度,不传weight就是度。做加权网络分析时忘了这个参数,度分布会直接算成拓扑度,和论文里的图对不上。
3.3 读写格式与可选依赖对照
1.11 的读写模块支持十几种格式,但依赖情况差别很大。搞清楚哪几个是「零依赖」的,能省掉很多装包的时间。
| 格式 | 读 / 写函数 | 额外依赖 | 常见坑 |
|---|---|---|---|
| 边表 | read_edgelist/write_edgelist | 无 | 节点默认是字符串,要nodetype=int;带权重要写data=(('weight', float),) |
| 邻接表 | read_adjlist/write_adjlist | 无 | 每行第一列是节点,后面是邻居,不能有表头 |
| GML | read_gml/write_gml | 无 | 默认relabel=True会把节点 id 换成 label,数值属性要靠 GML 里的类型声明 |
| GraphML | read_graphml/write_graphml | 无 | 属性必须显式声明类型,否则全变成字符串 |
| GEXF | read_gexf/write_gexf | 无 | 适合给图形工具做交换,属性类型比较宽松 |
| Pajek | read_pajek/write_pajek | 无 | 老社会网络分析软件的格式,节点名有字符限制 |
| JSON(node-link) | json_graph.node_link_data/node_link_graph | 无 | 适合塞进接口返回值,注意link和edge的命名差异 |
| pickle | read_gpickle/write_gpickle | 无 | 跨 Python 大版本经常读不出来,别拿它做长期存储 |
| DOT | write_dot | pydot 或 pygraphviz | 只用于出图,回读不保证完整 |
| YAML | read_yaml/write_yaml | PyYAML | 依赖最重的一个,非必要不用 |
实际项目里我一般只保留两套:边表做输入输出(人眼可读、Git diff 友好、任何工具都能处理),GraphML 或 GEXF 做带属性的交换(属性不丢,图形工具能直接打开)。pickle 只用于临时缓存,且必须和解释器版本绑定在一起归档,否则半年后自己都读不回来。
读写时的参数细节值得单拎出来说。read_edgelist的data参数是个元组序列,写法是data=(('weight', float),),少写一层括号或者把float写成字符串'float',读进来的权重就全是字符串,后面算最短路直接报类型错误。read_gml的relabel参数更要留心,默认True意味着你文件里的数字 id 会变成 label 字符串,做子图匹配时对不上。
3.4 从 2.x/3.x 回移到 1.11 的改写清单
如果你手上是一份新版本写的代码,要在 1.11 上跑,按这张表改基本能覆盖八成问题。
| 新版本写法 | 1.11 对应写法 | 说明 |
|---|---|---|
G.nodes[n]['k'] | G.node[n]['k'] | 容器改名,最密集的一处 |
G.edges[u, v]['k'] | G.edge[u][v]['k']或G[u][v]['k'] | 后者更短,1.11 支持 |
for n in G.nodes | for n in G.nodes() | 少一对括号就报TypeError |
nx.connected_components直接len() | len(list(nx.connected_components(G))) | 返回值类型有差异 |
G.nodes_iter() | G.nodes() | 方向相反:1.x 有,2.x 删了 |
nx.spring_layout(G, seed=42) | 先固定numpy.random.seed/random.seed | 老版本布局函数不吃seed关键字 |
nx.info返回字符串 | 同左,但字段更少 | 别指望它给出依赖信息 |
改的时候有个技巧:先用grep -n "\.nodes\[" *.py把可疑点全捞出来,再逐个确认是「属性访问」还是「方法调用」。属性访问要改成G.node[...],方法调用要保持带括号。混着改最容易改出TypeError: 'method' object is not subscriptable,而且报错行号往往已经偏了,定位起来很烦。
4. 跑通一条链路:建图、算中心性、出图存盘
单独验证 API 只能说明装对了,真正要做的是把一条完整的分析链路跑通:原始边表进来,清洗建图,算出几个常用指标,最后落盘出图。这一章用一份虚构的小型协作网络数据当例子,节点是人,边是协作关系,带一个合作次数当权重。整套代码在 1.11 上可以原样跑,不需要 numpy,只有出图那一步要 matplotlib。
4.1 建图选型与边表清洗
选图类型只看三个问题:关系有没有方向、两点之间会不会有多条边、要不要存属性。协作关系无向、两人之间可能多次合作但我选择合并加权,所以用Graph;如果要做信息传播方向分析就换DiGraph;如果多次合作必须分开记录,就用MultiGraph,代价是大部分算法要额外处理平行边。
import csv import networkx as nx G = nx.Graph() with open('edges.csv', newline='', encoding='utf-8') as f: reader = csv.reader(f) next(reader, None) # 跳过表头 for row in reader: if len(row) < 2: continue # 残缺行直接丢 u, v = row[0].strip(), row[1].strip() if not u or not v or u == v: continue # 空节点、自环一律不要 w = float(row[2]) if len(row) > 2 and row[2] else 1.0 if G.has_edge(u, v): G[u][v]['weight'] += w # Graph 重复加边会覆盖,这里手工累加 else: G.add_edge(u, v, weight=w) print(nx.info(G))这段逻辑的核心在G.has_edge(u, v)那个分支。Graph.add_edge是幂等的:同一对节点加两次,第二次只是更新属性,边数不会变成 2。很多人拿Graph处理带重复记录的边表,跑完发现边数比预期少一半,就是栽在这里。要么用MultiGraph,要么像我这样先判断再累加。
G[u][v]['weight'] += w是 1.11 支持的简写,等价于G.edge[u][v]['weight'] += w。w的默认值给 1.0,是为了兼容只有两列的边表;float()转换不能省,CSV 读出来全是字符串,不转的话后面做加权最短路会直接抛TypeError。u == v的自环过滤也要显式写,自环会让度中心性和聚类系数的解释变得很别扭。
4.2 指标计算:从度到介数
指标不要一次性全算,先算便宜的看着数据形状对不对,再决定要不要上贵的。度分布、连通分量是 O(n+m),秒出;介数中心性是 O(nm),几千节点就要等几分钟。
import networkx as nx # 便宜的:规模、连通性、度 print('节点', G.number_of_nodes(), '边', G.number_of_edges()) comps = sorted((len(c) for c in nx.connected_components(G)), reverse=True) print('连通分量大小', comps[:5]) print('最大连通子图占比', comps[0] / G.number_of_nodes()) deg_c = nx.degree_centrality(G) print('度中心性 top3', sorted(deg_c.items(), key=lambda x: -x[1])[:3]) # 中等:聚类系数,衡量局部抱团程度 print('平均聚类系数', nx.average_clustering(G, weight=None)) # 贵的:介数中心性,大图用 k 抽样 bc = nx.betweenness_centrality(G, k=200, normalized=True, weight='weight') # 排序类:PageRank,注意默认 weight='weight' pr = nx.pagerank(G, alpha=0.85, weight='weight') # 单源最短路径:source 指定起点,weight 不写就是跳数 dist = nx.shortest_path_length(G, source='n001', weight='weight') print('到 n001 最远的点', max(dist.items(), key=lambda x: x[1]))参数逐个说清楚。average_clustering的weight=None表示按拓扑结构算,不按权重加权;如果传weight='weight',聚类系数会偏向高权重三角形,含义完全变了,写论文时这点必须交代明白。betweenness_centrality的k=200表示只抽 200 个起点做近似,k=None(默认)才是精确计算;normalized=True把结果归一到 [0,1],跨图比较时必须开;weight='weight'表示按权重算距离,与不传完全不同。pagerank的alpha是阻尼系数,0.85 是惯例值,调大到 0.95 会让分数更集中在少数节点上;它的weight默认就是'weight',也就是说图里有weight属性就自动按权重算,这点和shortest_path_length的默认行为相反,特别容易搞混。
如果图规模上万,betweenness_centrality就别硬算了,改成在最大连通子图上算,或者把k压到 100 以内。我一般会先跑一遍nx.number_connected_components,把巨型分量之外的碎点单独列出来,再做指标,否则碎片节点的中心性指标会让整体排序失真。
4.3 落盘与出图:别在无显示环境上翻车
存盘优先选能保留属性的格式。GraphML 通用性最好,GEXF 对图形工具更友好,两个都写一遍成本很低。
import matplotlib matplotlib.use('Agg') # 必须在 import pyplot 之前,服务器上没有显示设备 import matplotlib.pyplot as plt import networkx as nx nx.write_gexf(G, 'graph.gexf') nx.write_graphml(G, 'graph.graphml') # 老版本布局函数没有 seed 参数,先固定随机源再算布局 import random random.seed(42) pos = nx.spring_layout(G, iterations=100, weight='weight') nx.draw_networkx_nodes(G, pos, node_size=30, alpha=0.8) nx.draw_networkx_edges(G, pos, alpha=0.3) plt.axis('off') plt.savefig('graph.png', dpi=200, bbox_inches='tight')matplotlib.use('Agg')放在import pyplot之前是硬性顺序,反过来写不生效。在服务器、容器、CI 里跑,用默认交互后端轻则报no display name,重则进程挂住不返回,看不出任何错误信息,这就是典型的黑匣子式翻车。
spring_layout的iterations控制力导向迭代次数,100 对几百节点的图够用,节点上万时要降到 20~50,否则时间全花在布局上。1.11 的布局函数不接受seed关键字,想复现同一张图就得提前固定random.seed,这是老版本和新版本在可复现性上最实际的差别。weight='weight'让布局考虑边权,强关系的节点会靠得更近,可视化效果更符合直觉,但也会让稠密区域的点挤成一团,视情况取舍。
出图只适合几百到几千节点。再往上,节点和边会糊成一个色块,这时候要么只画最大连通子图,要么按度过滤掉低度节点,要么换用外部布局工具算好坐标再回填到pos字典里——draw_networkx_nodes接受任意{节点: (x, y)}坐标,这条路很实用。
5. 避坑排查:老版本在新解释器上的五种翻车
下面这五条,是我在把 1.11 塞进各种环境时反复遇到的。每条都按「现象 → 原因 → 解决」写,看到类似报错可以直接对号入座,不用从头查。
第一种:import成功,但一碰G.nodes就TypeError: 'method' object is not iterable。
现象是代码能导入、能建图,遍历节点时崩。原因几乎总是版本对不上——环境里实际生效的是 2.x/3.x,G.nodes是方法而不是视图,for n in G.nodes:就是在迭代一个方法对象。解决办法是先定位:python -c "import networkx; print(networkx.__version__, networkx.__file__)",看输出是不是 1.11 和你挂的路径。如果不是,pip uninstall networkx卸干净再装 zip;如果是在 venv 里,注意别在激活前就跑了脚本,很多「明明装了却没用上」都是这个原因。
第二种:Python 3.10 及以上报AttributeError: module 'collections' has no attribute 'Mapping'。
现象是导入阶段直接挂,堆栈指向 networkx 内部某个模块。原因是解释器在 3.10 把collections下的Mapping、MutableMapping、Iterable这批抽象基类移走了,只剩collections.abc里的版本,而老代码还在按老路径取。解法有两条:把解释器降到 3.8/3.9,或者在第 6 章那个兼容垫片里提前把名字补回去。3.11 还要多一层,inspect.getargspec也被删了,光补collections不够。
第三种:pip install networkx-1.11.zip报subprocess-exited-with-error,日志里全是setup.py install is deprecated。
现象是 pip 拉起来就失败,日志很长但没有明确错误。原因是这个 sdist 没有pyproject.toml,新版 pip 默认建一个隔离的构建环境,里面没有 setuptools,跑到setup.py就崩。解决办法是加--no-build-isolation,并确认当前环境里pip install -U setuptools wheel已经执行过。如果还不行,就按 2.2 的第一条路径:手工解压,cd进去,python -m pip install . --no-build-isolation,这样能看到完整的setup.py输出,比读 pip 的折叠日志快得多。
第四种:GML 读进来节点 id 全变成字符串,数值属性变成'3.0'这种带引号的字符串。
现象是图结构没问题,但一算加权最短路就报类型错误,或者节点匹配全失败。原因有两层:read_gml默认relabel=True,会把文件里的数字 id 替换成 label;同时 GML 的属性类型依赖文件里的type声明,声明缺失或写成string就全部退化成字符串。解决办法是读的时候显式传relabel=False保住原始 id,写的时候保证属性值是int/float而不是numpy标量或字符串。这类问题最麻烦的地方在于它不报错,只是静默地改变数据,属于典型的「跑通了但结果是错的」。
第五种:nx.draw在服务器上卡住不返回,或者存出来的图一片空白。
现象是脚本跑完没有输出文件,或者文件只有几 KB 的白图。原因是在没有显示设备的环境里用了交互式后端,plt.show()在等一个不存在的窗口;或者savefig在draw之前就执行了。解决办法是matplotlib.use('Agg')写在import pyplot之前,去掉所有plt.show(),并确认savefig在绘图调用之后。另外dpi给小了(默认 100)也会让大图糊成一团,200~300 比较合适。
6. 进阶收口:把 1.11 关进 venv 并加一层兼容垫片
零散地修报错不如一次性把环境封死。我现在的做法固定成三步:独立 venv、版本写进文件、兼容垫片前置。第一步前面讲过,重点是把 venv 目录和 zip 一起归档——老包的问题往往不是装不上,而是半年后原封不动地重现当时那个环境。归档时顺手记下pip freeze > requirements-lock.txt和 zip 的 sha256,比任何文档都可靠。
第二步是把兼容垫片做成自动生效的。venv 的site-packages根目录下放一个sitecustomize.py,解释器启动时会自动导入,不需要改任何业务代码:
# sitecustomize.py —— 放在 venv 的 site-packages 根目录,启动时自动执行 import collections import collections.abc import inspect from collections import namedtuple # 3.10 起 collections 下的 ABC 被移除,按老路径补回去 for _name in ("Mapping", "MutableMapping", "Sequence", "Iterable", "Callable", "Hashable", "Set", "MutableSet"): if not hasattr(collections, _name): setattr(collections, _name, getattr(collections.abc, _name)) # 3.11 起 inspect.getargspec 被删,用 getfullargspec 拼一个同名替代 if not hasattr(inspect, "getargspec"): _ArgSpec = namedtuple("ArgSpec", "args varargs keywords defaults") def _getargspec(func): spec = inspect.getfullargspec(func) return _ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults) inspect.getargspec = _getargspec这段垫片只覆盖「按老名字取值」这一类用法。如果老代码依赖getargspec返回对象上的其他字段,或者依赖collections里被删掉的其他名字,还得逐个补。所以它是缓解措施,不是万能方案;能选 3.8/3.9 就别硬扛 3.11。
第三步是把验证写成一个可执行的冒烟脚本,退出码非 0 就阻断后续流程:
#!/usr/bin/env bash set -e python - <<'PY' import networkx as nx assert nx.__version__ == '1.11', nx.__version__ G = nx.Graph() G.add_edge('a', 'b', weight=2) assert nx.shortest_path_length(G, 'a', 'b', weight='weight') == 2 assert 'weight' in G.edge['a']['b'] print('smoke ok:', nx.__file__) PY关键约束整理成一张表,部署新机器时照着核一遍:
| 约束项 | 建议值 | 理由 |
|---|---|---|
| 解释器版本 | 3.8 / 3.9(必要时 3.7) | 3.10+ 移除 ABC,3.11+ 移除getargspec |
| 安装方式 | 解压后pip install . --no-build-isolation | 避免构建隔离导致的 setuptools 缺失 |
| 版本核验 | nx.__version__且检查 ` |
本文还有配套的精品资源,点击获取