以 Pythonic 方式使用 i > j ( > k) 迭代多个索引

Posted

技术标签:

【中文标题】以 Pythonic 方式使用 i > j ( > k) 迭代多个索引【英文标题】:Iterating over multiple indices with i > j ( > k) in a pythonic way 【发布时间】:2017-01-07 13:02:18 【问题描述】:

我需要遍历一组索引。所有索引必须在范围内 [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 会很大,因此我不想进行排序。

【问题讨论】:

【参考方案1】:

一般可以这样解决:

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 我想过,但它需要在产生之前对每个值做一些事情【参考方案2】:

这是一种使用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 的输出。看起来很有趣! @hiroprotagonist 好吧,它并不是一个生成器,因为我将它用作 NumPy 数组。所以,为了得到最终的输出,我们需要用元组包装它:tuple(map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1]))。这回答了你的问题? @hiroprotagonist 或者,如果您不介意将索引数组作为最终输出:N-1-np.array(list(combinations(range(N),M)))[::-1] 缺点是您创建了一个列表并将所有索引都保存在内存中;它不再是一个“纯”发电机,对吧?不幸的是islice 无法处理负面步骤... @hiroprotagonist 不,一旦我们用np.array() 将它包裹起来,它就不是生成器,因此会在内存中创建所有输出索引。会不会有问题?【参考方案3】:

如果您不介意丢弃大部分生成的元组的低效率,您可以使用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

【讨论】:

确实看起来很干净。谢谢!将不得不计算效率低下的严重程度...... 您可以使用product(*[range(5-x) for x in range(3)]) 来缓解一些低效问题(当然,53 可以根据需要进行修改),但这会降低优雅性,并没有真正完全解决问题。 【参考方案4】:

这是一种基于以下观察的方法缺点是需要在内存中创建列表:

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's answer 中的评论相同(带有def ijk(n, depth) 的那个),对吧?还是喜欢! 啊,不,现在我看到了区别! @hiroprotagonist 略有不同,因为我使用的是循环负值的范围——如果它完全一样,我就不会发布了。乔恩·夏普的似乎最好。【参考方案5】:

使用eval 有点“骇人听闻”的尝试(只是为了完整性而添加这个。这里有更好的答案!)。

这个想法是构造一个类似的字符串

'((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,并且和那些一样高效。

【讨论】:

以上是关于以 Pythonic 方式使用 i > j ( > k) 迭代多个索引的主要内容,如果未能解决你的问题,请参考以下文章

pythonic获取索引的方法,值列== 1

以pythonic方式打印字典中最大值的键[重复]

有没有一种更 Pythonic 的方式来在函数的参数上展开列表?

如何以 Pythonic 方式向上移动目录?

pythonic方式在没有索引变量的情况下做N次?

以块为单位迭代列表的最“pythonic”方式是啥?