以 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)])
来缓解一些低效问题(当然,5
和3
可以根据需要进行修改),但这会降低优雅性,并没有真正完全解决问题。 【参考方案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) 迭代多个索引的主要内容,如果未能解决你的问题,请参考以下文章