从 1.X 迁移到 2.0 指南#

这是一份针对从 NetworkX 1.X 迁移到 NetworkX 2.0 的用户的指南

关于这些问题的讨论可以在邮件列表上进行。

本文档底部将讨论如何编写同时兼容 NetworkX v1.x 和 v2.0 的代码。

我们对 Multi/Di/Graph 类中的方法进行了一些重大更改。更改的方法将在下面通过示例进行解释。

NetworkX 2.0 发布后,我们正在转向视图/迭代器报告 API。我们将许多方法的报告方式从返回列表或字典更改为迭代信息。这方面的多数更改都在基类中。过去返回容器的方法现在返回视图(灵感来源于 Python 中的字典视图),而返回迭代器的方法已被移除。创建新图的方法的数据复制深度已发生变化。G.subgraph/edge_subgraph/reverse/to_directed/to_undirected 受到了影响。现在许多方法提供了创建视图而不是复制数据的选项。数据复制的深度也可能已发生变化。

一个视图的例子是 G.nodes(或 G.nodes()),它现在返回一个类似字典的 NodeView,而 G.nodes_iter() 已被移除。类似地,G.edges 现在返回视图并移除了 G.edges_iter。图属性 G.nodeG.edge 已被移除,取而代之的是使用 G.nodes[n]G.edges[u, v]。最后,selfloop 方法以及 add_path/star/cycle 已从图方法移至 networkx 函数。

我们预计这些更改会破坏一些现有代码。我们尝试让这些更改以抛出异常的方式破坏代码,以便代码被破坏的情况显而易见。

在基础图类之外,代码库还有许多改进。这些改进太多,无法在此一一列举,但其中一些明显的包括

  • drawing/nx_pylab 中节点的居中,

  • 一些 shortest_path 例程的输出(迭代器 vs 字典)


一些示例

>>> import networkx as nx
>>> G = nx.complete_graph(5)
>>> G.nodes  # for backward compatibility G.nodes() works as well
NodeView((0, 1, 2, 3, 4))

你可以迭代遍历 G.nodes(或 G.nodes()

>>> for node in G.nodes:
...     print(node)
0
1
2
3
4

如果你想要一个节点列表,可以使用 Python 的 list 函数

>>> list(G.nodes)
[0, 1, 2, 3, 4]

G.nodes 类似于集合,允许进行集合操作。它也类似于字典,你可以使用 G.nodes[n]['weight'] 查找节点数据。你仍然可以使用调用接口 G.nodes(data='weight') 来迭代节点/数据对。除了类似字典的视图 keys/values/items 之外,G.nodes 还有一个数据视图 G.nodes.data(‘weight’)。新的 EdgeView G.edges 对边也有类似的功能。

通过添加视图,NetworkX 支持一些新功能,例如对视图执行集合操作。

>>> H = nx.Graph()
>>> H.add_nodes_from([1, 'networkx', '2.0'])
>>> G.nodes & H.nodes  # finding common nodes in 2 graphs
{1}
>>> # union of nodes in 2 graphs
>>> G.nodes | H.nodes  
{0, 1, 2, 3, 4, 'networkx', '2.0'}

类似地,G.edges 现在返回 EdgeView 而不是边的列表,并且它也支持集合操作。

>>> G.edges  # for backward compatibility G.nodes() works as well
EdgeView([(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)])
>>> list(G.edges)
[(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]

G.degree 现在返回一个 DegreeView。它不像其他视图那样完全像字典,因为它迭代的是 (node, degree) 对,不提供 keys/values/items/get 方法。但它确实提供了查找功能 G.degree[n](node, degree) 迭代。如果需要按节点键控到度数值的字典,可以轻松地通过 dict(G.degree) 创建。

>>> G.degree  # for backward compatibility G.degree() works as well
DegreeView({0: 4, 1: 4, 2: 4, 3: 4, 4: 4})
>>> G.degree([1, 2, 3])
DegreeView({1: 4, 2: 4, 3: 4})
>>> list(G.degree([1, 2, 3]))
[(1, 4), (2, 4), (3, 4)]
>>> dict(G.degree([1, 2, 3]))
{1: 4, 2: 4, 3: 4}
>>> G.degree
DegreeView({0: 4, 1: 4, 2: 4, 3: 4, 4: 4})
>>> list(G.degree)
[(0, 4), (1, 4), (2, 4), (3, 4), (4, 4)]
>>> dict(G.degree)
{0: 4, 1: 4, 2: 4, 3: 4, 4: 4}

可以通过 G.degree[node] 计算单个节点的度。有向图的 in_degreeout_degree 也做了类似的更改。如果你只想要度数值,这里有一些选项。它们以 DiGraphin_degree 为例展示,但类似思路也适用于 out_degreedegree

>>> DG = nx.DiGraph()
>>> DG.add_weighted_edges_from([(1, 2, 0.5), (3, 1, 0.75)])
>>> deg = DG.in_degree   # sets up the view
>>> [d for n, d in deg]   # gets all nodes' degree values
[1, 1, 0]
>>> (d for n, d in deg)    # iterator over degree values
<generator object <genexpr> ...>
>>> [deg[n] for n in [1, 3]]   # using lookup for only some nodes
[1, 0]
>>> for node, in_deg in dict(DG.in_degree).items():  # works for nx1 and nx2
...     print(node, in_deg)
1 1
2 1
3 0
>>> dict(DG.in_degree([1, 3])).values()    # works for nx1 and nx2
dict_values([1, 0])
>>> # DG.in_degree(nlist) creates a restricted view for only nodes in nlist.
>>> # but see the fourth option above for using lookup instead.
>>> list(d for n, d in DG.in_degree([1, 3]))
[1, 0]
>>> [len(nbrs) for n, nbrs in DG.pred.items()]  # probably slightly fastest for all nodes
[1, 1, 0]
>>> [len(DG.pred[n]) for n in [1, 3]]           # probably slightly faster for only some nodes
[1, 0]

如果 nG 中的一个节点,那么 G.neighbors(n) 返回一个迭代器。

>>> n = 1
>>> G.neighbors(n)
<dict_keyiterator object at ...>
>>> list(G.neighbors(n))
[0, 2, 3, 4]

DiGraphViews 的行为类似于 GraphViews,但有一些额外的方法。

>>> D = nx.DiGraph()
>>> D.add_edges_from([(1, 2), (2, 3), (1, 3), (2, 4)])
>>> D.nodes
NodeView((1, 2, 3, 4))
>>> list(D.nodes)
[1, 2, 3, 4]
>>> D.edges
OutEdgeView([(1, 2), (1, 3), (2, 3), (2, 4)])
>>> list(D.edges)
[(1, 2), (1, 3), (2, 3), (2, 4)]
>>> D.in_degree[2]
1
>>> D.out_degree[2]
2
>>> D.in_edges
InEdgeView([(1, 2), (2, 3), (1, 3), (2, 4)])
>>> list(D.in_edges())
[(1, 2), (2, 3), (1, 3), (2, 4)]
>>> D.out_edges(2)
OutEdgeDataView([(2, 3), (2, 4)])
>>> list(D.out_edges(2))
[(2, 3), (2, 4)]
>>> D.in_degree
InDegreeView({1: 0, 2: 1, 3: 2, 4: 1})
>>> list(D.in_degree)
[(1, 0), (2, 1), (3, 2), (4, 1)]
>>> D.successors(2)
<dict_keyiterator object at ...>
>>> list(D.successors(2))
[3, 4]
>>> D.predecessors(2)
<dict_keyiterator object at ...>
>>> list(D.predecessors(2))
[1]

同样的更改也适用于 MultiGraphs 和 MultiDiGraphs。


set_edge_attributesset_node_attributes 的参数顺序已发生变化。namevalues 的位置互换,并且 name 现在默认为 None。之前的调用签名 (graph, name, value) 已更改为 (graph, value, name=None)。新的风格允许省略 name,转而向 values 传递一个字典的字典。

将现有代码迁移到新版本的一个简单方法是显式指定关键字参数名称。这种方法向后兼容,并且无论参数顺序如何都能确保传递正确的参数。例如,旧代码

>>> G = nx.Graph([(1, 2), (1, 3)])
>>> nx.set_node_attributes(G, 'label', {1: 'one', 2: 'two', 3: 'three'})  
>>> nx.set_edge_attributes(G, 'label', {(1, 2): 'path1', (2, 3): 'path2'})  

在新版本中将导致 TypeError: unhashable type: 'dict'。代码可以重构为

>>> G = nx.Graph([(1, 2), (1, 3)])
>>> nx.set_node_attributes(G, name='label', values={1: 'one', 2: 'two', 3: 'three'})
>>> nx.set_edge_attributes(G, name='label', values={(1, 2): 'path1', (2, 3): 'path2'})

一些方法已从基础图类移至主包命名空间。它们是:G.add_path, G.add_star, G.add_cycle, G.number_of_selfloops, G.nodes_with_selfloops, 和 G.selfloop_edges。它们已被替换为 nx.path_graph(G, ...), nx.add_star(G, ...), nx.selfloop_edges(G) 等。为了向后兼容,我们将它们保留为已弃用的方法。


使用新的 GraphViews(SubGraph、ReversedGraph 等),你不能再假设 G.__class__() 会创建一个与 G 类型相同的新图实例。实际上,__class__ 的调用签名取决于 G 是视图还是基类。对于 v2.x,你应该使用 G.fresh_copy() 来创建一个正确类型的空图,以便填充节点和边。

图视图也可以是视图-的-视图-的-视图-的-图。如果你想找到链条末端的原始图,请使用 G.root_graph。但请注意,它可能与视图的图类型(有向/无向)不同。


topological_sort 不再接受 reversenbunch 参数。如果 nbunch 是单个节点源,那么现在可以使用 subgraph 运算符实现相同的效果

nx.topological_sort(G.subgraph(nx.descendants(G, nbunch)))

要实现反向拓扑排序,应将输出转换为列表

reversed(list(nx.topological_sort(G)))


编写同时兼容两个版本的代码#

方法 set_node_attributes/get_node_attributes/set_edge_attributes/get_edge_attributes 更改了其关键字参数 namevalues 的顺序。因此,为了使其与两个版本兼容,你应该在调用中使用关键字参数。

>>> nx.set_node_attributes(G, values=1.0, name='weight')

将名称中包含 _iter 的方法更改为不带 _iter 的版本。在 v1 中,这会将迭代器替换为列表,但代码仍然可以工作。在 v2 中,这会创建一个视图(其行为类似于迭代器)。


将任何使用 G.edge 的地方替换为 G.adj。图属性 edge 已被移除。属性 G.adj 在 v1 中就是 G.edge,因此它将与两个版本兼容。


如果你在 v1.x 中使用 G.node.items() 或类似方法,你可以将其替换为 G.nodes(data=True),这适用于 v2.x 和 v1.x。迭代遍历 G.node`,例如 for n in G.node:,可以用 G 替换,例如:for n in G:


图属性 node 的功能已移至 G.nodes,因此期望与 v2.x 兼容的代码应该使用 G.nodes。实际上,大多数使用 G.node 的地方都可以替换为兼容两个版本的写法。不容易兼容的功能是:G.node[n]。在 v2.x 中,这变成了 G.nodes[n],这在 v1.x 中不起作用。

幸运的是,如果你希望代码也能与 v1.x 兼容,在 v2.x 中仍然可以使用 G.node[n]。我们在 v2.x 中保留了 G.node 作为指向 G.nodes 的过渡指针。我们设想在 v3.x(将来的某个时候)移除 G.node


不正确地直接从一个图复制节点属性字典到另一个图可能会损坏节点数据结构。如下所示的代码

>>> # dangerous in v1.x, not allowed in v2.x
>>> G.node[n] = H.node[n]  

过去可以使用,即使 n 不是 G 中的节点也可能导致错误。该代码在 v2.x 中会引发错误。请将其替换为更安全的版本之一

>>> G.nodes[n].update(H.nodes[n])  # works in v2.x

从图类中移除并放入主包命名空间的方法可以通过相关的已弃用方法使用。如果你想将代码更新到新函数,一个使代码同时适用于两个版本的技巧是针对 v2.x 编写代码,并在 v1 命名空间中临时添加代码

>>> if nx.__version__[0] == '1':
...     nx.add_path = lambda G, nodes: G.add_path(nodes)

类似地,使用 G.fresh_copy()G.root_graph 的 v2.x 代码很难让其适用于 v1.x。在这种情况下,最好的做法可能是显式确定你想要的图类型,并直接调用 Graph/DiGraph/MultiGraph/MultiDiGraph。

在 v1 和 v2 中使用 Pickle#

Pickle 协议不存储类方法,只存储数据。因此,如果你使用 v1 写入一个 pickle 文件,不应该期望将其读入 v2 Graph。如果发生这种情况,请在安装 v1 的情况下读取它,并写入一个包含节点和边信息的文件。你可以在安装 v2 的情况下读取该配置,然后将这些节点和边添加到新图中。尝试类似下面的操作

>>> # in v1.x
>>> pickle.dump([G.nodes(data=True), G.edges(data=True)], file)  
>>> # then in v2.x
>>> nodes, edges = pickle.load(file)  
>>> G = nx.Graph()  
>>> G.add_nodes_from(nodes)  
>>> G.add_edges_from(edges)