【问题标题】:More effective loop in PythonPython中更有效的循环
【发布时间】:2014-12-24 08:22:51
【问题描述】:

我有一种情况,我需要遍历两个对象列表并找到相等,然后遍历它们的字段并更改一些属性。好像是这样的

for new_product in products_and_articles['products']:
  for old_product in products_for_update:
    if new_product.article == old_product.article:
      for old_field in old_product._meta.get_all_field_names():
        for new_field in new_product._meta.get_all_field_names():
          if old_field == new_field and old_field != 'id' and old_field != 'slug':
            setattr(old_product, old_field, getattr(new_product, old_field))

显然,这远非好,甚至不能接受。 所以我正在寻求建议如何避免这么多循环并增强算法

【问题讨论】:

  • 删除 new_field 循环?反正你也不用 new_field。
  • 另外,对两个列表进行排序,你会得到 nlogn 而不是 n^2
  • 你能把两个输入列表的简单例子和输出的简单例子放在一起吗?
  • 我在这里做的是在两个列表中搜索相同的产品,然后在匹配的对象中搜索相同的模型字段,然后用新数据更新旧对象的字段
  • 这看起来像 django,如果是这样 - 让数据库完成繁重的工作

标签: python django performance nested-loops


【解决方案1】:

如果您将流程分解为合乎逻辑的、可重复使用的部分,这会有所帮助。

for new_product in products_and_articles['products']:
  for old_product in products_for_update:
    if new_product.article == old_product.article:
      …

例如,您在这里所做的是查找与特定article 匹配的产品。由于article 是唯一的,我们可以这样写:

def find_products_by_article(products, article):
  '''Find all products that match the given article.  Returns
  either a product or 'None' if it doesn't exist.'''
  for products in products:
    return product

然后调用它:

for old_product in products_for_update:
  new_products = find_products_by_article(
                   products_and_articles['products'],
                   old_product.article)
  …

但是如果我们可以利用为查找优化的数据结构,即dict(常量而不是线性复杂度)。所以我们可以做的是:

# build a dictionary that stores products indexed by article
products_by_article = dict(product.article, product for product in
                           products_and_articles['products'])

for old_product in products_for_update:
  try:
    # look up product in the dictionary
    new_product = products_by_article[old_product.article]
  except KeyError:
    # silently ignore products that don't exist
    continue
  …

如果您经常进行此类查找,最好在其他地方重用products_by_article 字典,而不是每次都从头开始构建一个。 请注意:如果您使用产品记录的多个表示,则需要使它们始终保持同步!

对于内部循环,请注意这里的new_field 仅用于检查字段是否存在:

…
  for old_field in old_product._meta.get_all_field_names():
    for new_field in new_product._meta.get_all_field_names():
      if old_field == new_field and old_field != 'id' and old_field != 'slug':
        setattr(old_product, old_field, getattr(new_product, old_field))

(请注意,这有点可疑:old_product 中尚不存在的任何新字段都会被静默丢弃:这是故意的吗?)

这可以重新包装如下:

def transfer_fields(old, new, exclusions=('id', 'slug')):
  '''Update all pre-existing fields in the old record to have
  the same values as the new record.  The 'exclusions' parameter
  can be used to exclude certain fields from being updated.'''
  # use a set here for efficiency reasons
  fields = frozenset(old._meta.get_all_field_names())
  fields.difference_update(new._meta.get_all_field_names())
  fields.difference_update(exclusions)
  for field in fields:
    setattr(old, field, getattr(new, field))

将所有这些放在一起:

# dictionary of products indexed by article
products_by_article = dict(product.article, product for product in
                           products_and_articles['products'])

for old_product in products_for_update:
  try:
    new_product = products_by_article[old_product.article]
  except KeyError:
    continue          # ignore non-existent products
  transfer_fields(old_product, new_product)

这个最终代码的时间复杂度为O(n × k),其中n 是产品数,k 是字段数。

【讨论】:

  • 请注意,你也组织得很好,它仍然是 O(n^2*k)。
  • 是的,这可以通过重新设计数据结构来提高效率,但我把它留给了 OP 作为练习。另外,我不知道article 是否是唯一键——它在如何完成时会有所不同。
  • 能否请您写下您对重新设计的建议,因为我希望有一天管理层会说“让这些东西正常工作”之类的话:)
  • 完成。 (您本身不必重新设计,您可以只在本部分代码中使用新的数据结构。)
【解决方案2】:

您可以使用set 来查找交集,而不是遍历两个列表并检查是否相等:

set(products_and_articles['products']).intersection(set(products_for_update))

示例:

>>> l=[1,2,3]
>>> a=[2,3,4]
>>> set(l).intersection(set(a))
set([2, 3])

【讨论】:

  • 我的意见集在这里不是一个好方法,因为每个列表可能有重复的项目,并且集将删除它们。
  • @Urb 是的,但是因为它不会改变主列表,并且操作只是想要交叉点,我认为它很好!
  • 请查看我上面的评论
  • 很抱歉,但它比这更复杂。您处理的对象不是数字 - 它们是对象,因此您要么必须从 a.article 中形成集合,要么在对象中实现哈希方法。即使这样做,要将属性从旧属性转移到新属性,您也需要对两个对象都有引用;您将需要在循环中搜索,无法绕过它。
【解决方案3】:

我们从四个循环开始,效率为O(n^2*k^2),n 是项目数,k 是属性数。让我们看看我们能做些什么。

首先,去掉new_product循环,你不需要它:

for old_field in old_product._meta.get_all_field_names():
    for new_field in new_product._meta.get_all_field_names():
        if old_field == new_field and old_field != 'id' and old_field != 'slug':
            setattr(old_product, old_field, getattr(new_product, old_field))

收件人:

for old_field in old_product._meta.get_all_field_names():
    if old_field != 'id' and old_field != 'slug':
        setattr(old_product, old_field, getattr(new_product, old_field))

得到了 O(n^2*k)。现在是产品查找部分。

首先,对两个列表进行排序,然后像在合并排序中合并列表时那样继续:

a = sorted(products_and_articles['products'], key=lambda x: x.article)
b = sorted(products_for_update, key=lambda x: x.article)
i = j = 0
while(i < len(a) and j < len(b)):
    if (a[i].article < b[j].article):
        a += 1
        continue
    if (a[i].article > b[j].article):
        b += 1
        continue
    ...logic...
    a += 1  # Maybe you want to get rid of this one, I'm not sure..
    b += 1

根据您的数据库大小,它可能或多或少足够,因为它需要您创建新的排序列表。内存不是很重(无论如何它只是 refs),但如果您的列表非常长且空间有限,那么巨大的效率优势可能无法弥补。

找到O(n*logn*k),这是我能做的最好的了。您可能可以使用字典将其降低,但这需要您更改数据库,因此需要更多时间和精力。

【讨论】:

    【解决方案4】:

    前两个for可以改成:

    from itertools import product
    
    
    for new_product, old_product in product(list1, list2)
        # logic and other loops
    

    你可以对两个内部循环做同样的事情:

     for old_field in old_product._meta.get_all_field_names():
        for new_field in new_product._meta.get_all_field_names():
    
    for old_field, new_field in product(list1, list2)
    

    【讨论】:

    • 这只会加倍时间.. 我认为组合和 for 循环之间没有太大区别,而且它们也属于同一个列表。 .
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-18
    • 2016-04-14
    • 2016-04-15
    • 2014-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多