使用许多属性和字典查找优化 Python 代码

Posted

技术标签:

【中文标题】使用许多属性和字典查找优化 Python 代码【英文标题】:Optimizing Python code with many attribute and dictionary lookups 【发布时间】:2010-04-05 18:22:38 【问题描述】:

我用 Python 编写了一个程序,它花费大量时间从字典键中查找对象的属性和值。我想知道是否有任何方法可以优化这些查找时间,可能使用 C 扩展,以减少执行时间,或者我是否需要简单地用编译语言重新实现程序。

该程序使用图形实现了一些算法。它在我们的数据集上运行得非常缓慢,因此我使用cProfile 使用可以实际完成的缩减数据集来分析代码。 大量大部分时间都消耗在一个函数中,特别是在函数内的两个语句(生成器表达式)中:

第 202 行的生成器表达式是

    neighbors_in_selected_nodes = (neighbor for neighbor in
            node_neighbors if neighbor in selected_nodes)

第 204 行的生成器表达式是

    neighbor_z_scores = (interaction_graph.node[neighbor]['weight'] for
            neighbor in neighbors_in_selected_nodes)

此上下文函数的源代码如下。

selected_nodesset 中的节点 interaction_graph,这是一个 NetworkX Graph 实例。 node_neighbors 是来自 Graph.neighbors_iter() 的迭代器。

Graph 本身使用字典来存储节点和边。它的Graph.node 属性是一个字典,将节点及其属性(例如'weight')存储在属于每个节点的字典中。

这些查找中的每一个都应按常数时间摊销(即 O(1)),但是,我仍然为查找付出了巨大的代价。有什么方法可以加快这些查找速度(例如,通过将其中的一部分编写为 C 扩展),还是需要将程序移至编译语言?


以下是提供上下文的函数的完整源代码;绝大多数执行时间都花在了这个函数上。

def calculate_node_z_prime(
        node,
        interaction_graph,
        selected_nodes
    ):
    """Calculates a z'-score for a given node.

    The z'-score is based on the z-scores (weights) of the neighbors of
    the given node, and proportional to the z-score (weight) of the
    given node. Specifically, we find the maximum z-score of all
    neighbors of the given node that are also members of the given set
    of selected nodes, multiply this z-score by the z-score of the given
    node, and return this value as the z'-score for the given node.

    If the given node has no neighbors in the interaction graph, the
    z'-score is defined as zero.

    Returns the z'-score as zero or a positive floating point value.

    :Parameters:
    - `node`: the node for which to compute the z-prime score
    - `interaction_graph`: graph containing the gene-gene or gene
      product-gene product interactions
    - `selected_nodes`: a `set` of nodes fitting some criterion of
      interest (e.g., annotated with a term of interest)

    """
    node_neighbors = interaction_graph.neighbors_iter(node)
    neighbors_in_selected_nodes = (neighbor for neighbor in
            node_neighbors if neighbor in selected_nodes)
    neighbor_z_scores = (interaction_graph.node[neighbor]['weight'] for
            neighbor in neighbors_in_selected_nodes)
    try:
        max_z_score = max(neighbor_z_scores)
    # max() throws a ValueError if its argument has no elements; in this
    # case, we need to set the max_z_score to zero
    except ValueError, e:
        # Check to make certain max() raised this error
        if 'max()' in e.args[0]:
            max_z_score = 0
        else:
            raise e

    z_prime = interaction_graph.node[node]['weight'] * max_z_score
    return z_prime

这里是根据 cProfiler 的前几个调用,按时间排序。

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
156067701  352.313    0.000  642.072    0.000 bpln_contextual.py:204(<genexpr>)
156067701  289.759    0.000  289.759    0.000 bpln_contextual.py:202(<genexpr>)
 13963893  174.047    0.000  816.119    0.000 max
 13963885   69.804    0.000  936.754    0.000 bpln_contextual.py:171(calculate_node_z_prime)
  7116883   61.982    0.000   61.982    0.000 method 'update' of 'set' objects

【问题讨论】:

为什么是两个循环? neighbors_in_selected_nodesneighbor_z_scores?为什么不是一个循环?两步法似乎没有引入任何新东西。为什么这样做?您能否更新问题以解释为什么使用两种理解而不是一种? 我只想考虑 1) 邻居和 2) 选择的节点的权重(存在于 selected_nodes 中)。我使用两个生成器表达式来做到这一点:neighbors_in_selected_nodes 过滤器用于选择的节点;这被链接到neighbor_z_scores 以检索权重。链式迭代器应该使它们成为一个循环,而不是两个;循环在max() 内进行评估。 这两个生成器表达式确实可以写成一个 for 循环;第一个表达式表示语句if neighbor in selected_nodes:,第二个表达式表示neighbor_z_scores.append(interaction_graph.node[neighbor]['weight'])。通过使用生成器表达式,我避免了创建列表和附加操作。从获取邻居直到评估max(neighbor_z_scores),我完全处理迭代器链。 【参考方案1】:

如何保持interaction_graph.neighbors_iter(node) 的迭代顺序排序(或使用collections.heapq 部分排序)?由于您只是试图找到最大值,您可以按降序迭代node_neighbors,selected_node中的第一个节点必须是selected_node中的最大值。

其次,selected_node 多久会改变一次?如果它很少更改,您可以通过“interaction_graph.node[neighbor] for x in selected_node”列表来节省大量迭代,而不必每次都重新构建此列表。

编辑:回复 cmets

一个 sort() 需要 O(n log n)

不一定,您看课本太多了。不管你的教科书怎么说,你有时可以通过利用数据的某些结构来打破 O(n log n) 的障碍。如果您首先将邻居列表保存在自然排序的数据结构中(例如 heapq、二叉树),则无需在每次迭代时重新排序。当然,这是一个时空权衡,因为您需要存储冗余的邻居列表,并且代码复杂性要确保邻居列表在邻居更改时更新。

另外,python 的 list.sort(),它使用timsort 算法,对于几乎排序的数据非常快(在某些情况下可能平均 O(n))。它仍然没有破坏 O(n log n),这已被证明在数学上是不可能的。

您需要在放弃解决方案之前进行分析,因为它不太可能提高性能。在进行极端优化时,您可能会发现在某些非常特殊的边缘情况中,旧而慢的冒泡排序可能会胜过美化的快速排序或归并排序。

【讨论】:

排序是一个有趣的想法。 sort() 将采用 O(n log n)(其中 n 是邻居的数量);这将比线性搜索更昂贵。根据heapq 文档,heapify()O(n),但我认为每个流行音乐要么是O(log n) 要么是O(n)(不确定),这意味着在最坏的情况下,它仍然是操作数的两倍通过邻居的简单线性循环。 为了解决第二点,不幸的是,selected_nodes 会随着每次调用此函数而改变,因此它不适合缓存。不过,好主意。【参考方案2】:

我不明白为什么您的“权重”查找必须采用 ["weight"](节点是字典?)而不是 .weight(节点是对象)的形式。

如果您的节点是对象,并且没有很多字段,您可以利用the __slots__ directive 来优化它们的存储:

class Node(object):
    # ... class stuff goes here ...

    __slots__ = ('weight',) # tuple of member names.

编辑:所以我查看了您提供的 NetworkX 链接,有几件事让我感到困扰。首先是,在顶部,“字典”的定义是“FIXME”。

总的来说,它似乎坚持使用字典,而不是使用可以被子类化的类来存储属性。虽然对象上的属性查找可能本质上是字典查找,但我看不出使用对象会如何更糟。如果有的话,它可能会更好,因为对象属性查找更有可能被优化,因为:

对象属性查找是如此普遍, 对象属性的键空间比字典键更受限制,因此可以在搜索中使用优化的比较算法,并且 对象具有__slots__ 优化,正是针对这些情况,您的对象只有几个字段并且需要优化对它们的访问。

例如,我经常在表示坐标的类上使用__slots__。对我来说,树节点似乎是另一个明显的用途。

所以当我阅读时:

节点 节点可以是任何可散列的 Python 对象,但 None 除外。

我想,好吧,没问题,但紧接着就是

节点属性 在添加节点或分配给指定节点 n 的 G.node[n] 属性字典时,节点可以使用关键字/值对将任意 Python 对象分配为属性。

我想,如果一个节点需要属性,为什么要单独存储呢?为什么不把它放在节点中?使用contentStringweight 成员编写课程是否有害?边缘似乎更疯狂,因为它们被规定为元组而不是您可以子类化的对象。

所以我对 NetworkX 背后的设计决策感到迷茫。

如果你坚持使用它,我建议将属性从这些字典移动到实际节点中,或者如果这不是一个选项,使用整数作为键而不是字符串,这样搜索使用的速度要快得多比较算法。

最后,如果你将你的生成器组合起来会怎样:

neighbor_z_scores = (interaction_graph.node[neighbor]['weight'] for
        neighbor in node_neighbors if neighbor in selected_nodes)

【讨论】:

好问题。节点本身只是代表生物实体的 Python 字符串 (ID)。每个节点的权重属性使用其节点属性结构存储在图中:networkx.lanl.gov/reference/glossary.html#term-node-attribute 由于属性查找本质上是字典查找,我不确定切换到真正的属性是否会提高性能。 性能提升将是避免字符串比较,将其替换为 id 比较......或类似的东西。语言不是我写的,但我很确定 foo.bar'bar': 123['bar'] 生成更好的字节码,因为前者的情况要多得多。 迈克,凭直觉,我也相信你,但我刚刚在比较中得出的计时结果显示并非如此。 bitbucket.org/gotgenes/interesting-python-timings/src 总结是,对于 Python 2.6.4,类属性访问 __slots__ 访问 __slots__ 快大约 10%。【参考方案3】:

尝试直接访问 dict 并捕获 KeyErrors,这可能会更快,具体取决于您的命中/未命中率:

# cache this object
ignode = interaction_graph.node
neighbor_z_scores = []
for neighbor in node_neighbors:
    try:
        neighbor_z_scores.append(ignode[neighbor]['weight'])
    except KeyError:
        pass

或使用 getdefault 和列表推导:

sentinel = object()
# cache this object 
ignode = interaction_graph.node

neighbor_z_scores = (ignode[neighbor]['weight'] for neighbor in node_neighbors)
# using identity testing, it's slightly faster
neighbor_z_scores = (neighbor for neighbor in neighbor_z_scores if neighbor is not sentinel)

【讨论】:

不是selected_nodes 成员的邻居会比成员多得多,所以我首先过滤这个标准。这意味着更少的'weight' 查找。我可以保证所有'weight' 查找都会成功,因此那里的try-except 子句没有任何好处。不过,ignode 是一个非常好的主意,并且会针对该属性进行多次查找。【参考方案4】:

在不深入研究您的代码的情况下,尝试使用itertools 增加一点速度。

在模块级别添加这些:

import itertools as it, operator as op
GET_WEIGHT= op.attrgetter('weight')

变化:

neighbors_in_selected_nodes = (neighbor for neighbor in
        node_neighbors if neighbor in selected_nodes)

进入:

neighbors_in_selected_nodes = it.ifilter(selected_node.__contains__, node_neighbors)

和:

neighbor_z_scores = (interaction_graph.node[neighbor]['weight'] for
        neighbor in neighbors_in_selected_nodes)

进入:

neighbor_z_scores = (
    it.imap(
        GET_WEIGHT,
        it.imap(
            interaction_graph.node.__getitem__,
            neighbors_in_selected_nodes)
    )
)

这些有帮助吗?

【讨论】:

以上是关于使用许多属性和字典查找优化 Python 代码的主要内容,如果未能解决你的问题,请参考以下文章

使用 dict.items() 为大型数据集优化字典查找

快速搜索许多字符串以查找许多字典键

__slots__ 如何避免字典查找?

经过4次优化我把python代码耗时减少95%

python 性能优化技巧

一个类应该有多个属性或一个属性作为具有许多键的字典,Python3