【问题标题】:Logically merging physically distinct dictionaries逻辑上合并物理上不同的字典
【发布时间】:2012-01-21 20:45:04
【问题描述】:

我正在尝试为两个字典编写一个包装器,以使它们看起来像一个字典(仅供阅读;写作应该引发异常)。

我这样做是为了节省内存,因为其他地方需要原始字典之一。我还认为,如果要查找组合字典中不到一半的元素,它比合并字典要快。

这是我的尝试:

class LogicalMerge:
  def __init__(self, d1, d2):
    #d1 and d2 are dictionaries
    self.d1 = d1
    self.d2 = d2
  def __getitem__(self, x):
    if x in self.d1:
      return self.d1[x]
    else:
      return self.d2[x]

d1 = {1:2, 3:4}
d2 = {5:10}
d = LogicalMerge(d1, d2)
d[1] # == 2
d[5] # == 10

这种方法是否存在任何设计、技术或性能问题?

【问题讨论】:

  • 两个dict之间会有不对称吗?喜欢一个比另一个长或更多使用?或者像上次使用的可能会再次使用?
  • 将在别处重复使用的是d2。 d1 除了在LogicalMerge.__init__ 之外,在任何地方都没有使用(也只有一次)。除此之外,我不能说哪个字典更大,它们将按什么顺序被访问等等。

标签: python performance python-3.x


【解决方案1】:

您可以通过将__getitem__ 重写为每次调用来节省一次查找

try:
    return self.d1[x]
except KeyError:
    return self.d2[x]

这遵循“请求宽恕,而不是许可”的蟒蛇习语。

我也觉得比合并字典快

我强烈怀疑这一点,但你应该确定。您的方法引入了额外的间接级别,并且需要对密钥进行多次散列。它也必然会占用更多内存。

编辑:这是另一种方法。用DictWithBackup 替换你的一个dicts,它的行为类似于dict,除了缺少一个键时,它会查看另一个dict。

class DictWithBackup(dict):
    def __init__(self, backup):
         self._backup = backup

    def __missing__(self, key):
         return self._backup[key]

这个版本避免了异常处理。

【讨论】:

  • 它不应该占用更多内存,并且 try/except 版本可能会更慢:设置 try/except 块不是“免费”的,我敢打赌它需要更多的资源而不是额外的在其中一个字典中查找。
  • 我强烈支持“请求宽恕,而不是许可”,但如果this question's answers 是准确的,我认为在这种情况下,try/except 块将比 if 块慢得多。 d2 中的每个项目都不会在 d1 中(根据 max 的 cmets 之一),因此我们将为 d2 中的每个项目获得一个例外。如果 d2 的大小与 d1 的大小相当,那么会有很多例外。我想这个成语适用于例外很少的情况..
  • @chrisn654: Python - Is a dictionary slow to find frequency of each character? 表示.setdefault()(查找)可能比try/except 慢。最好使用真实输入数据和分析器来回答性能问题。
  • @larsmans:“多次散列” - 这是一个很好的观点。但是,合并字典的内存成本太高,所以不是一个选择。我不得不吃任何时间成本。
【解决方案2】:

出于性能原因,我更喜欢以下内容。鉴于None 是一个永远不会有效发生的对象。

def __getitem__(self, k):
  v = self.d1.get(k, None)
  if v is None:
    v = self.d2[k] # if you're going to raise an error anyway ...
  return v

否则,您可以默认获取自定义对象。请注意,您要么需要一个实现__eq__ 的对象来测试值相等性(o1 == o2),要么——性能方面甚至更好——你使用一个不可变对象,即某个字符串"error_key_not_found_string",这不是每次都新创建的。然后你甚至可以通过对象身份id(o1) == id(o2) 进行比较,即使用is 运算符。 (您也不需要提供__eq__。)

def __getitem__(self, k):
  v = self.d1.get(k, "error_key_not_found_string")
  # if id(v) == id("error_key_not_found_string":
  if v is "error_key_not_found_string": 
    v = self.d2[k] # if you're going to raise an error anyway ...
  return v

您是否考虑过这个项目在两个词典中的位置?

总之,从设计的角度来看,我觉得这有点令人困惑。性能提升真的证明了额外的错误和混乱来源是合理的吗?另外,您将失去所有其他 dict 功能...... 这可以像d1.update(d2) 一样简单。鉴于d1 是您在其他地方不使用的字典(那么您可以使用deepcopy)。

【讨论】:

  • 如果两个字典都包含相同的键,我会在程序的早期知道这一点,并引发异常。至于这样做的理由,我选择进行逻辑而不是物理合并以节省内存而不是运行时。两个字典都很大,其中一个 (d2) 必须保持不变。
  • 归结为问题性能真的很重要吗?当然只有你能回答。如果没有,我更喜欢更清晰的代码。
  • 如果性能如此关键,他可以使用(请耐心等待):v = self.d1.get(k, self.d2.get(k, None)),然后是单个 if v is None: raise KeyError(k)。
  • 如果代码稍微复杂一点,性能会很好。然而,内存使用是绝对关键的。
  • @moooeeeep:你肯定不会得到嵌套.get()的快捷方式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多