【问题标题】:transform a definiton of a list from iteration to comprehension将列表的定义从迭代器转换为理解
【发布时间】:2019-01-31 21:07:58
【问题描述】:

我有一个采用边列表的方法,边具有这种形式:(v1,v2,capacity) 并返回这种形式的字典:

dico = {v1:{v2:capacity,v3:capacity} v2:...}

这个dico 代表一个图

我想通过理解的方式定义这个 dico,但我真的被阻止了,我什至不确定我们是否可以这样做,所以有人可以告诉我这是否可能吗? 这是我的功能:

def _init_from_edges(self, edges):
    self._G={}
    for e in edges:
        if e[0] in self._G:
            self._G[e[0]][e[1]]=e[2]
        else:
            self._G[e[0]]={e[1]:e[2]}

【问题讨论】:

  • 不知道为什么你会被驱使去使用理解(对我来说,这是 Python 的那些特性之一,在我看来,它似乎更多地是由“gee-whiz we can do this in a single line”而不是需要可维护的代码)当您显示的代码非常简单时 - 尽管我看不到它在哪里创建 v3:capacity 除非您在边缘列表中重复此操作。请显示您为边缘列表假设的一些示例内容。
  • 事实上,请在您的问题中编辑您当前代码的最小完整可验证示例stackoverflow.com/help/mcve
  • @barny:一般的理解没有错。 当他们做简单的事情时,它们(通常)更快,并且(通常)比等效循环更易于阅读。正确使用(作为无副作用的功能构造),它们向维护者暗示代码足够简单,可以作为一个输入到另一个输入的简单过滤器/转换(其中正常循环不受限制) -影响,即使按照惯例)。也就是说,我同意 OP 的特殊情况不会从理解中受益;它可以工作,但不应该。
  • 我对理解的功能或速度没有任何抱怨,当它们不起作用时,它们对调试的不可渗透性 - 即它们的不可维护性 - 使它们成为我很少使用的 Python 功能。我不想鼓励任何人将它们用于真正的代码。为了好玩,为了教育,很好——递归是类似的——但不适用于其他人可能必须尝试理解和更新的真实代码。
  • 我今天遇到了这个问题——我不认为这是使用推导式的一个闪亮的理由,因为这两行代码很容易使用一个 for 循环和一个额外的行来实现更清楚。 empty_props = [p for p,v in list(self.predicates.items()) if not v] list(map(lambda k: self.predicates.pop(k, None), empty_props)) 与第二行构造一个未使用的列表仅作为一种弹出方式。这是理解-蒙昧主义——我敢肯定还有更糟糕的例子,但我的观点是理解并不总是更好

标签: python python-3.x dictionary-comprehension


【解决方案1】:

您的代码不能轻易利用 dict 推导,因为它实际上是一个 multidict(其中一个键没有一个值,而是多个值)。

你可以稍微简化一下代码with collections.defaultdict:

from collections import defaultdict

def _init_from_edges(self, edges):
    self._G = defaultdict(dict)
    for v1, v2, capacity in edges:
        self._G[v1][v2] = capacity
    # Optional: Remove defaultdict behaviors after building
    self._G = dict(self._G)

使用defaultdict(dict) 意味着当一个键不在字典中时,它会立即使用全新的dict 创建,因此您根本不需要执行成员资格测试。

请注意,我还使用对命名变量进行解包而不是重复索引,以使代码更具自记录性。

使用实际的dict 理解来完成这项工作的唯一方法是:

  1. 为每个输入重新扫描一次edges 以收集给定v1 的所有v2/capacity 对(但那是O(n**2),所以如果edges 可以很大,这是个坏主意)
  2. 提前将每个v1 的所有值打包在一起,这样每个子dict 可以一次构建。

由于选项 #1 通常非常浪费,作为 dict 理解而不一遍又一遍地重新扫描 edges 的唯一实用方法是选项 #2,您可以这样做O(n log n) 带排序 followed by itertools.groupby:

from itertools import groupby
from operator import itemgetter

def _init_from_edges(self, edges):
    self._G = {v1: {v2: capacity for _, v2, capacity in grp}
               for v1, grp in groupby(sorted(edges, key=itemgetter(0)),
                                      key=itemgetter(0))}

这需要O(n log n) 工作对edges 进行排序(如果edges 已经排序,Python 的 TimSort 意味着它更接近O(n) 工作),然后O(n) 工作对结果进行分组。比{v1: {v2: capacity for v, v2, capacity in edges if v == v1} for v1, _, _ in edges} 快,但仍然比使用defaultdict 的非理解方法慢(在所有情况下都是O(n))。

【讨论】:

    【解决方案2】:

    这有点令人费解,但它似乎有效:

    edges = [(1, 2, 3),
             (1, 3, 4),
             (2, 1, 5),
             (2, 3, 6),
             (2, 2, 7)]
    dico = {v1: {v2: cap for v, v2, cap in edges if v == v1} for v1, _, _ in edges}
    # {1: {2: 3, 3: 4}, 2: {1: 5, 2: 7, 3: 6}}
    

    基本上,对于每个v1,它会将edges 中的每个键:值对与v1 相加。外部理解不需要v2 或cap,所以它只是使用下划线来忽略这些值。内部循环需要将v1 的其值与外部循环的值进行比较,这就是它使用不同名称的原因。

    【讨论】:

    • 注意:这是一个O(n**2) 解决方案,因为它必须对edges 中的每个v1 执行edges 的完整线性扫描(为每个@987654333 构造相同的dict @ 与 v1 出现在 edges 中的次数一样多)。虽然它不会减少大 O(因为您不能假设任何 v1 出现多次),但您至少可以将有效成本降低到 O(n * m),其中 m 是唯一的 v1 值,通过将 for v1, _, _ in edges 更改为 for v1 in {v1 for v1, _, _ in edges},这使得 v1s 可以迭代,因此您只扫描 edges 并构造一个新的 sub-dict 每个唯一的 @987654345 @.
    • 虽然我认识到您正在回答 OP 的问题/挑战,并且我钦佩您将字典结构压缩为一行代码的决心,但我看不出有人会如何维护该代码 - 所以我看不出它有任何实际价值。干得好。
    猜你喜欢
    • 2012-04-24
    • 1970-01-01
    • 2020-01-03
    • 1970-01-01
    • 1970-01-01
    • 2011-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多