【问题标题】:How can I, in python, iterate over multiple 2d lists at once, cleanly?在 python 中,我怎样才能干净地一次迭代多个 2d 列表?
【发布时间】:2008-10-09 20:32:13
【问题描述】:

例如,如果我正在制作一个简单的基于网格的游戏,我可能会有一些 2d 列表。一个可能用于地形,另一个可能用于对象等。不幸的是,当我需要遍历列表并让一个列表中的正方形的内容影响另一个列表的一部分时,我必须做这样的事情。

for i in range(len(alist)):
    for j in range(len(alist[i])):
        if alist[i][j].isWhatever:
            blist[i][j].doSomething()

有没有更好的方法来做这样的事情?

【问题讨论】:

    标签: python


    【解决方案1】:

    如果有人对上述解决方案的性能感兴趣,这里是 4000x4000 网格,从最快到最慢:

    编辑:增加了Brian的分数,修改izip,大获全胜!

    John 的解决方案也非常快,尽管它使用索引(看到这一点我真的很惊讶!),而 Robert 和 Brian 的(zip)比问题创建者的初始解决方案慢。

    所以让我们来介绍Brian 的获胜函数,因为它在此线程中的任何地方都没有以正确的形式显示:

    from itertools import izip
    for a_row,b_row in izip(alist, blist):
        for a_item, b_item in izip(a_row,b_row):
            if a_item.isWhatever:
                b_item.doSomething()
    

    【讨论】:

    • 您按原样尝试了 Brian 的解决方案,还是使用 itertools.izip?另外,您是否也愿意对我的建议进行基准测试?
    • 已添加,结果再次令人惊讶。在您的情况下,似乎向对象添加意外(不在 slots 中)变量非常慢!
    • 哦,初始化可以和已有的矩阵初始化合并。我不知道你为什么提到“不在slots”部分;我们不知道单元格对象是如何实现的。顺便说一句,您忘记调用 a_item.isWhatever (即没有括号)
    • 好吧,我使用了带有“普通”(即字典)属性管理的虚拟对象。至于括号,在原始问题中没有:)
    • 这很有趣。我原以为在 isWhatever 很少见的情况下原版会表现更好,因为它根本不需要访问 blist ,但看起来索引查找更重要。即使在 0.5% 的情况下,izip 方法也做得更好。
    【解决方案2】:

    我先写一个生成器方法:

    def grid_objects(alist, blist):
        for i in range(len(alist)):
            for j in range(len(alist[i])):
                yield(alist[i][j], blist[i][j])
    

    然后,每当您需要遍历列表时,您的代码如下所示:

    for (a, b) in grid_objects(alist, blist):
        if a.is_whatever():
            b.do_something()
    

    【讨论】:

    • 这不是一回事,在第二个范围内取了alist[i]的len,你为什么去掉那个索引?
    • 我绝对最喜欢这个。这可能不是最好的答案,但我很有可能会使用它。
    • 这只是 Eugene 所写内容的语法糖。如果网格足够大,我担心这个生成器可能会非常慢。毕竟,每个收益仍然需要四次索引查找。
    • 另外,明智的做法是将 range() 调用更改为 xrange()。
    【解决方案3】:

    你可以压缩它们。即:

    for a_row,b_row in zip(alist, blist):
        for a_item, b_item in zip(a_row,b_row):
            if a_item.isWhatever:
                b_item.doSomething()
    

    但是,如果您很少实际使用 b_item(即 a_item.isWhatever 通常为 False),则压缩和迭代项目的开销可能会高于您的原始方法。您可以使用 itertools.izip 而不是 zip 来减少它对内存的影响,但它仍然可能会稍微慢一些,除非您总是需要 b_item。

    或者,考虑使用 3D 列表,因此单元格 i,j 的地形位于 l[i][j][0],对象位于 l[i][j][1] 等,或者甚至组合对象,因此您可以执行 a[i][j].terrain、a[i][j].object 等。

    [Edit] DzinX's timings 实际上表明对 b_item 进行额外检查的影响并不显着,仅次于按索引重新查找的性能损失,因此上述(使用 izip)似乎是最快的.

    我现在也对 3d 方法进行了快速测试,它似乎更快,所以如果您可以以这种形式存储数据,那么访问起来会更简单、更快捷。这是一个使用它的例子:

    # Initialise 3d list:
    alist = [ [[A(a_args), B(b_args)] for i in xrange(WIDTH)] for j in xrange(HEIGHT)]
    
    # Process it:
    for row in xlist:
        for a,b in row:
            if a.isWhatever(): 
                b.doSomething()
    

    以下是我使用 1000x1000 数组进行 10 次循环的时间,isWhatever 的不同比例为:

                ( Chance isWhatever is True )
    Method      100%     50%      10%      1%
    
    3d          3.422    2.151    1.067    0.824
    izip        3.647    2.383    1.282    0.985
    original    5.422    3.426    1.891    1.534
    

    【讨论】:

    • 这是这里最快的解决方案,但前提是您将 zip 更改为 itertools.izip(请参阅下面某处的帖子)。
    【解决方案4】:

    当您使用数字网格并希望获得非常好的性能时,您应该考虑使用Numpy。它使用起来非常简单,让您可以考虑使用网格而不是网格上的循环来进行操作。性能来自这样一个事实,即这些操作随后使用优化的 SSE 代码在整个网格上运行。

    例如,这里有一些使用我编写的代码的 numpy,它对通过弹簧连接的带电粒子进行蛮力数值模拟。此代码在 31 毫秒内计算具有 100 个节点和 99 条边的 3d 系统的时间步长。这比我能想到的最好的纯 Python 代码快 10 倍以上。

    from numpy import array, sqrt, float32, newaxis
    def evolve(points, velocities, edges, timestep=0.01, charge=0.1, mass=1., edgelen=0.5, dampen=0.95):
        """Evolve a n body system of electrostatically repulsive nodes connected by
           springs by one timestep."""
        velocities *= dampen
    
        # calculate matrix of distance vectors between all points and their lengths squared
        dists = array([[p2 - p1 for p2 in points] for p1 in points])
        l_2 = (dists*dists).sum(axis=2)
    
        # make the diagonal 1's to avoid division by zero
        for i in xrange(points.shape[0]):
            l_2[i,i] = 1
    
        l_2_inv = 1/l_2
        l_3_inv = l_2_inv*sqrt(l_2_inv)
    
        # repulsive force: distance vectors divided by length cubed, summed and multiplied by scale
        scale = timestep*charge*charge/mass
        velocities -= scale*(l_3_inv[:,:,newaxis].repeat(points.shape[1], axis=2)*dists).sum(axis=1)
    
        # calculate spring contributions for each point
        for idx, (point, outedges) in enumerate(izip(points, edges)):
            edgevecs = point - points.take(outedges, axis=0)
            edgevec_lens = sqrt((edgevecs*edgevecs).sum(axis=1))
            scale = timestep/mass
            velocities[idx] += (edgevecs*((((edgelen*scale)/edgevec_lens - scale))[:,newaxis].repeat(points.shape[1],axis=1))).sum(axis=0)
    
        # move points to new positions
        points += velocities*timestep
    

    【讨论】:

      【解决方案5】:

      作为一个轻微的风格变化,你可以使用枚举:

      for i, arow in enumerate(alist):
          for j, aval in enumerate(arow):
              if aval.isWhatever():
                  blist[i][j].doSomething()
      

      除非您按照 Federico 的建议重新排列数据结构,否则我认为您不会得到任何简单得多的东西。这样你就可以把最后一行变成“aval.b.doSomething()”之类的东西。

      【讨论】:

        【解决方案6】:

        Generator expressions 和来自itertools moduleizip 在这里会做得很好:

        from itertools import izip
        for a, b in (pair for (aline, bline) in izip(alist, blist) 
                     for pair in izip(aline, bline)):
            if a.isWhatever:
                b.doSomething()
        

        上面for语句中的那一行表示:

        • 从组合网格alistblist 中取出每一行,并从它们中创建一个元组(aline, bline)
        • 现在再次将这些列表与 izip 组合并从中获取每个元素 (pair)。

        这种方法有两个优点:

        • 在任何地方都没有使用索引
        • 您不必使用 zip 创建列表,而是使用更高效的生成器 izip

        【讨论】:

        • 我相信是的,是的。通过删除索引,您可以更清楚地了解您在这里要完成的工作(操作对象,而不是它们的位置)。
        • 我认为迭代对象比迭代数字更干净。一个不错的方法。
        【解决方案7】:

        您确定您正在并行迭代的两个矩阵中的对象是概念上不同类的实例吗?如果合并两个类,最终得到一个包含 both isWhatever() 和 doSomething() 的对象矩阵,该怎么办?

        【讨论】:

          【解决方案8】:

          如果两个 2D 列表在游戏的生命周期内保持不变并且你不能享受 Python 的多重继承来加入 alist[i][j] 和 blist[i][j ] 对象类(正如其他人建议的那样),您可以在创建列表后在每个 a 项中添加一个指向相应 b 项的指针,如下所示:

          for a_row, b_row  in itertools.izip(alist, blist):
              for a_item, b_item in itertools.izip(a_row, b_row):
                  a_item.b_item= b_item
          

          各种优化可以在这里应用,比如你的类定义了__slots__,或者上面的初始化代码可以与你自己的初始化代码等合并。之后,您的循环将变为:

          for a_row in alist:
              for a_item in a_row:
                  if a_item.isWhatever():
                      a_item.b_item.doSomething()
          

          这样应该更有效率。

          【讨论】:

          • 这绝对是一个意想不到的答案。很有趣。
          【解决方案9】:

          如果a.isWhatever 很少为真,您可以建立一个“索引”一次:

          a_index = set((i,j) 
                        for i,arow in enumerate(a) 
                        for j,a in enumerate(arow) 
                        if a.IsWhatever())
          

          每次你想做某事时:

          for (i,j) in a_index:
              b[i][j].doSomething()
          

          如果随着时间的推移发生变化,那么您将需要 使索引保持最新。这就是为什么我用 一组,因此可以快速添加和删除项目。

          【讨论】:

            【解决方案10】:
            for d1 in alist
               for d2 in d1
                  if d2 = "whatever"
                      do_my_thing()
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2022-11-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-10-16
              • 2021-08-17
              • 1970-01-01
              相关资源
              最近更新 更多