以Pythonic的方式迭代多个索引,其中i > j ( > k)。

14

我需要遍历一个索引元组。所有索引必须在范围[0, N)内,并满足条件i > j。我在这里提供了一个简单的示例,只涉及两个索引;我需要将其扩展到三个(具有i > j > k)或更多。

基本版本如下:

N = 5
for i in range(N):
    for j in range(i):
        print(i, j)

它运行得非常好;输出结果为

1 0
2 0
2 1
3 0
3 1
3 2
4 0
4 1
4 2
4 3

我不想为每个额外的索引增加一个缩进级别,因此我更喜欢这个版本:

for i, j in ((i, j) for i in range(N) for j in range(i)):
    print(i, j)

这个工作得非常好,做了它应该做的事情,并且消除了多余的缩进级别。

我希望能够拥有更加优雅的解决方案(对于两个指数来说不是很相关,但对于三个或更多个指数来说就变得更加重要)。到目前为止,我想出了这个:

from itertools import combinations

for j, i in combinations(range(N), 2):
    print(i, j)

这可以很好地遍历相同的索引对。唯一不同的是出现的索引对的顺序:

1 0
2 0
3 0
4 0
2 1
3 1
4 1
3 2
4 2
4 3
由于我正在处理这些索引的顺序很重要,因此我无法使用这个。是否有一种简洁、短小、Pythonic的方法,可以按照第一个示例产生的相同顺序迭代这些索引?请记住,N将会很大,所以排序不是我想要做的事情。
5个回答

17
您可以按照以下方式一般地解决此问题:
def indices(N, length=1):
    """Generate [length]-tuples of indices.

    Each tuple t = (i, j, ..., [x]) satisfies the conditions 
    len(t) == length, 0 <= i < N  and i > j > ... > [x].

    Arguments:
      N (int): The limit of the first index in each tuple.
      length (int, optional): The length of each tuple (defaults to 1).

    Yields:
      tuple: The next tuple of indices.

    """
    if length == 1:
       for x in range(N):
           yield (x,)
    else:
       for x in range(1, N):
            for t in indices(x, length - 1):
                yield (x,) + t

使用中:

>>> list(indices(5, 2))
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2), (4, 3)]
>>> list(indices(5, 3))
[(2, 1, 0), (3, 1, 0), (3, 2, 0), (3, 2, 1), (4, 1, 0), (4, 2, 0), (4, 2, 1), (4, 3, 0), (4, 3, 1), (4, 3, 2)]

也许yield from(从Python 3.3开始支持)可以使代码更加简洁,例如yield from range(N) - lazzzis
@lazzzis 我考虑过这个,但在生成每个值之前它需要执行某些操作。 - jonrsharpe

6

如果你不在意丢弃大部分生成的元组,那么可以使用itertools中的product函数。(随着repeat参数的增加,效率会变得更低。)

>>> from itertools import product
>>> for p in ((i,j) for (i,j) in product(range(5), repeat=2) if i > j):
...   print p
...
(1, 0)
(2, 0)
(2, 1)
(3, 0)
(3, 1)
(3, 2)
(4, 0)
(4, 1)
(4, 2)
(4, 3)
>>> for p in ((i,j,k) for (i,j,k) in product(range(5), repeat=3) if i > j > k):
...   print p
...
(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)
(4, 1, 0)
(4, 2, 0)
(4, 2, 1)
(4, 3, 0)
(4, 3, 1)
(4, 3, 2)

更新:不使用元组拆包,而是使用索引进行过滤。这样可以更紧凑地编写代码。只需针对不同大小的元组更改my_filter即可。
from itertools import product, ifilter
def my_filter(p):
    return p[0] > p[1] > p[2]

for p in ifilter(my_filter, product(...)):
    print p

看起来非常干净。谢谢!我得测一下效率有多差... - hiro protagonist
你可以通过 product(*[range(5-x) for x in range(3)]) 来减少一些低效率(当然,53 可以根据需要进行修改),但这会降低代码的优雅性,而且并不能完全解决问题。 - chepner

6
这里有一个使用itertools.combinations的方法来实现具有通用层数的技巧 -
map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1])

或者使用相同的方法进行扭曲:

map(tuple,np.array(list(combinations(range(N-1,-1,-1),M)))[::-1])

在这里,N代表元素的数量,M代表级别的数量。

示例运行 -

In [446]: N = 5
     ...: for i in range(N):
     ...:     for j in range(i):
     ...:         for k in range(j):  # Three levels here
     ...:             print(i, j, k)
     ...:             
(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)
(4, 1, 0)
(4, 2, 0)
(4, 2, 1)
(4, 3, 0)
(4, 3, 1)
(4, 3, 2)

In [447]: N = 5; M = 3

In [448]: map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1])
Out[448]: 
[(2, 1, 0),
 (3, 1, 0),
 (3, 2, 0),
 (3, 2, 1),
 (4, 1, 0),
 (4, 2, 0),
 (4, 2, 1),
 (4, 3, 0),
 (4, 3, 1),
 (4, 3, 2)]

啊哈!重新排列combinations的输出。看起来很有趣! - hiro protagonist
@hiroprotagonist 好吧,它并不是一个生成器,因为我正在将其用作NumPy数组。因此,为了获得最终输出,我们需要使用元组进行包装:tuple(map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1]))。这回答了你的问题吗? - Divakar
@hiroprotagonist 如果您不介意将索引数组作为最终输出:N-1-np.array(list(combinations(range(N),M)))[::-1] - Divakar
缺点是你创建了一个列表并将所有的索引都存在内存中;它不再是一个“纯粹”的生成器,对吗?不幸的是,islice 无法处理负数步长... - hiro protagonist
@hiroprotagonist 是的,那应该可以,但似乎仍会在内存中创建这些索引。 - Divakar
显示剩余2条评论

2

这是一种基于观察的方法,其核心思想是按照(倒序)所需顺序生成索引的负值,这比直接生成索引要容易。它类似于 @Divakar 的方法,但也像那样需要在内存中创建列表,存在这个缺点。

def decreasingTuples(N,k):
    for t in reversed(list(itertools.combinations(range(1-N,1),k))):
        yield tuple(-i for i in t)

>>> for t in decreasingTuples(4,2): print(t)

(1, 0)
(2, 0)
(2, 1)
(3, 0)
(3, 1)
(3, 2)
>>> for t in decreasingTuples(4,3): print(t)

(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)

是的,这与我在Divakar的答案中评论的代码相同(即带有def ijk(n, depth)的代码),对吧?我仍然喜欢它! - hiro protagonist
啊,不,现在我看到了区别! - hiro protagonist
@hiroprotagonist 有点不同,因为我使用了一个循环负值的范围--如果完全相同,我就不会发帖了。Jon Sharpe的看起来最好。 - John Coleman

-1

使用eval的一种有点“hacky”的尝试(只是为了完整性而添加这个。这里还有更好的答案!)。

思路是构建一个字符串,类似于

'((a, b, c) for a in range(5) for b in range(a) for c in range(b))'

并返回其eval的值:

def ijk_eval(n, depth):
    '''
    construct a string representation of the genexpr and return eval of it...
    '''

    var = string.ascii_lowercase
    assert len(var) >= depth > 1  # returns int and not tuple if depth=1

    for_str = ('for {} in range({}) '.format(var[0], n) +
               ' '.join('for {} in range({})'.format(nxt, cur)
                        for cur, nxt in zip(var[:depth-1], var[1:depth])))
    return eval('(({}) {})'.format(', '.join(var[:depth]), for_str))

可以这样使用并产生正确的结果。

for i, j in ijk_eval(n=5, depth=2):
    print(i, j)

结构不是很好看,但结果是:它是一个常规的genexpr,并且和其他的一样高效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接