【问题标题】:How can I make a for-loop pyramid more concise in Python? [duplicate]如何在 Python 中使 for 循环金字塔更简洁? [复制]
【发布时间】:2015-04-07 13:53:36
【问题描述】:

在固体力学中,我经常使用 Python 并编写如下代码:

for i in range(3):
    for j in range(3):
        for k in range(3):
            for l in range(3):
                # do stuff

我经常这样做,以至于我开始怀疑是否有更简洁的方法可以做到这一点。当前代码的缺点是:如果我遵守PEP8,那么我不能超过每行 79 个字符的限制,并且没有太多剩余空间,特别是如果这又是一个类的函数。

【问题讨论】:

  • 你只是迭代范围吗?然后有一个更短(虽然不一定更易读)的方式。
  • 如果一个算法是 O(n^4),那么它是 O(n^4)。没办法。要绕过 79 个字符的限制,请考虑将它们拆分为函数。这将为可读性和可测试性创造奇迹。
  • 嗯...深度嵌套循环不是一种很好的编程方式...所以我认为您应该更担心避免深度嵌套循环而不是 PEP8。
  • 使用向量化的numpy操作如numpy.einsum(),见Fast tensor rotation with NumPy
  • 重复?这绝对是一个更好的问题......

标签: python performance python-2.7 python-3.x


【解决方案1】:

根据您想要做的事情,您可以使用itertools 模块来最小化for 循环(或zip)。在这种情况下itertools.product 将创建您对4 个循环所做的事情:

>>> list(product(range(3),repeat=4))
[(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 1, 0), (0, 0, 1, 1),
 (0, 0, 1, 2), (0, 0, 2, 0), (0, 0, 2, 1), (0, 0, 2, 2), (0, 1, 0, 0),
 (0, 1, 0, 1), (0, 1, 0, 2), (0, 1, 1, 0), (0, 1, 1, 1), (0, 1, 1, 2),
 (0, 1, 2, 0), (0, 1, 2, 1), (0, 1, 2, 2), (0, 2, 0, 0), (0, 2, 0, 1),
 (0, 2, 0, 2), (0, 2, 1, 0), (0, 2, 1, 1), (0, 2, 1, 2), (0, 2, 2, 0),
 (0, 2, 2, 1), (0, 2, 2, 2), (1, 0, 0, 0), (1, 0, 0, 1), (1, 0, 0, 2),
 (1, 0, 1, 0), (1, 0, 1, 1), (1, 0, 1, 2), (1, 0, 2, 0), (1, 0, 2, 1),
 (1, 0, 2, 2), (1, 1, 0, 0), (1, 1, 0, 1), (1, 1, 0, 2), (1, 1, 1, 0),
 (1, 1, 1, 1), (1, 1, 1, 2), (1, 1, 2, 0), (1, 1, 2, 1), (1, 1, 2, 2),
 (1, 2, 0, 0), (1, 2, 0, 1), (1, 2, 0, 2), (1, 2, 1, 0), (1, 2, 1, 1),
 (1, 2, 1, 2), (1, 2, 2, 0), (1, 2, 2, 1), (1, 2, 2, 2), (2, 0, 0, 0),
 (2, 0, 0, 1), (2, 0, 0, 2), (2, 0, 1, 0), (2, 0, 1, 1), (2, 0, 1, 2),
 (2, 0, 2, 0), (2, 0, 2, 1), (2, 0, 2, 2), (2, 1, 0, 0), (2, 1, 0, 1),
 (2, 1, 0, 2), (2, 1, 1, 0), (2, 1, 1, 1), (2, 1, 1, 2), (2, 1, 2, 0),
 (2, 1, 2, 1), (2, 1, 2, 2), (2, 2, 0, 0), (2, 2, 0, 1), (2, 2, 0, 2),
 (2, 2, 1, 0), (2, 2, 1, 1), (2, 2, 1, 2), (2, 2, 2, 0), (2, 2, 2, 1),
 (2, 2, 2, 2)]

在你的代码中你可以这样做:

for i,j,k,l in product(range(3),repeat=4):
    #do stuff

这个函数等价于下面的代码,只是实际的实现不会在内存中建立中间结果:

def product(*args, **kwds):
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = map(tuple, args) * kwds.get('repeat', 1)
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)

编辑:正如@PeterE 在评论product() 中所说,即使范围的长度不同,也可以使用:

product(range(3),range(4),['a','b','c'] ,some_other_iterable)

【讨论】:

  • 恕我直言,这是要走的路。至少如果循环与 OP 中介绍的一样基本。该方案还可以处理不同长度的范围(实际上它可以处理任意迭代):product(range(3),range(4),['a','b','c'] ,some_other_iterable)。如果嵌套循环比这更复杂(例如,包含一些 if/else 逻辑),那么定义你的 on generator 函数是下一个最好的事情。在这里,由于简单易懂,我更喜欢 @SergeBallesta 的变体而不是 @L3viathan 的生成器。
  • @PeterE 我认为您可以在for i,j,k,l in product(range(3),repeat=4) 中找到最简单的方法,我还添加了product 的等效功能来回答您可以查看。
  • 事后看来,我的评论可能被误解为批评家。这不是我的意图。我想表达的是,我发现您的解决方案最适合给定问题。我想提一下,即使范围的长度不同,product() 仍然可以使用。
  • @PeterE 我明白你的意思,感谢你的关注,请注意,我也会添加它来回答;)
  • 这是在这种情况下的唯一方法,只需一点 FP 概念和代码很快就会变得更加优雅/简洁。
【解决方案2】:

使用itertools.product 的想法很好。这是一种更通用的方法,可以支持不同大小的范围。

from itertools import product

def product_of_ranges(*ns):
    for t in product(*map(range, ns)):
        yield t

for i, j, k in product_of_ranges(4, 2, 3):
    # do stuff

【讨论】:

    【解决方案3】:

    它不会更简洁,因为它会花费你一个生成器函数,但至少你不会被 PEP8 困扰:

    def tup4(n):
        for i in range(n):
            for j in range(n):
                for k in range(n):
                    for l in range(n):
                        yield (i, j, k, l)
    
    for (i, j, k, l) in tup4(3):
        # do your stuff
    

    (在 python 2.x 中,您应该在生成器函数中使用 xrange 而不是 range

    编辑:

    当金字塔的深度已知时,上述方法应该没问题。但是你也可以在没有任何外部模块的情况下制作一个通用生成器:

    def tup(n, m):
        """ Generate all different tuples of size n consisting of integers < m """
        l = [ 0 for i in range(n)]
        def step(i):
            if i == n : raise StopIteration()
            l[i] += 1
            if l[i] == m:
                l[i] = 0
                step(i+ 1)
        while True:
            yield tuple(l)
            step(0)
    
    for (l, k, j, i) in tup(4, 3):
        # do your stuff
    

    (我使用(l, k, j, i),因为在上面的生成器中,第一个索引首先变化)

    【讨论】:

      【解决方案4】:

      这是等价的:

      for c in range(3**4):
          i = c // 3**3 % 3
          j = c // 3**2 % 3
          k = c // 3**1 % 3
          l = c // 3**0 % 3
          print(i,j,k,l)
      

      如果您一直这样做,请考虑使用通用生成器:

      def nestedLoop(n, l):
          return ((tuple((c//l**x%l for x in range(n-1,-1,-1)))) for c in range(l**n))
      
      for (a,b,c,d) in nestedLoop(4,3):
          print(a,b,c,d)
      

      【讨论】:

      • c // 1?无论如何,好点......如果你至少展示了如何将这个特定案例变成一个生成器,我可能会赞成。
      • 在这种情况下,出于说明目的,您可能还应该使用c // 3**3 % 3c // 3**2 % 3;-)
      • ……还有发电机。
      • 同意...但是对于这里的大多数人来说,这似乎并不像简短那么重要。您可以通过 yield 使用元组而不是返回生成器表达式来进一步缩短它,以及将 reversed(range(n)) 更改为 range(n)[::-1](或 range(n,-1,-1))。
      • @martineau 后者,因为我认为切片会生成一个列表。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-21
      • 1970-01-01
      • 1970-01-01
      • 2021-12-09
      • 2015-12-14
      • 2014-10-25
      相关资源
      最近更新 更多