Iterate over pairs in a list (circular fashion) in Python
Asked Answered
C

13

37

The problem is easy, I want to iterate over each element of the list and the next one in pairs (wrapping the last one with the first).

I've thought about two unpythonic ways of doing it:

def pairs(lst):
    n = len(lst)
    for i in range(n):
        yield lst[i],lst[(i+1)%n]

and:

def pairs(lst):
    return zip(lst,lst[1:]+lst[:1])

expected output:

>>> for i in pairs(range(10)):
    print i

(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, 5)
(5, 6)
(6, 7)
(7, 8)
(8, 9)
(9, 0)
>>> 

any suggestions about a more pythonic way of doing this? maybe there is a predefined function out there I haven't heard about?

also a more general n-fold (with triplets, quartets, etc. instead of pairs) version could be interesting.

Casabonne answered 10/8, 2009 at 21:50 Comment(4)
Your first solution is good enough! – Larghetto
In the second version, change lst[0] to lst[:1] to make it work for an empty sequence. The code becomes more symmetrical, too. – Halsy
In thes second version, +[lst[:1]] should be replaced by +lst[:1], isn't it ? – Wiskind
@ManuelSelva you are right, it was [lst[0]] when I wrote it, I don't know why @colonelpanic changed it 🀷 – Casabonne
K
31
def pairs(lst):
    i = iter(lst)
    first = prev = item = i.next()
    for item in i:
        yield prev, item
        prev = item
    yield item, first

Works on any non-empty sequence, no indexing required.

Kirimia answered 10/8, 2009 at 21:57 Comment(5)
I like the stream quality to it. And the use of iter() is polished. – Cyn
@Darius, if you need to support empty seq's, use next(i, sentinel) in lieu of the old-fashioned i.next(), after setting sentinel = object(); and, if first is sentinel: return just before the for. – Elenore
@Darius: I have clarified that the sequence must be non-empty. It's debatable what it should do for an empty sequence: version 1 of the OP yields an empty sequence, and version two raises IndexError. – Chela
Crashes on a 1-element sequence: after calling i.next() the sequence is empty, so the loop doesn't bind item, so yield item, first raises an UnboundLocalError. – Engraving
For python 3: replace i.next() with next(i) and you're all set. And it actually also works on python 2. – Kamat
C
9

I've coded myself the tuple general versions, I like the first one for it's ellegant simplicity, the more I look at it, the more Pythonic it feels to me... after all, what is more Pythonic than a one liner with zip, asterisk argument expansion, list comprehensions, list slicing, list concatenation and "range"?

def ntuples(lst, n):
    return zip(*[lst[i:]+lst[:i] for i in range(n)])

The itertools version should be efficient enough even for large lists...

from itertools import *
def ntuples(lst, n):
    return izip(*[chain(islice(lst,i,None), islice(lst,None,i)) for i in range(n)])

And a version for non-indexable sequences:

from itertools import *
def ntuples(seq, n):
    iseq = iter(seq)
    curr = head = tuple(islice(iseq, n))
    for x in chain(iseq, head):
        yield curr
        curr = curr[1:] + (x,)

Anyway, thanks everybody for your suggestions! :-)

Casabonne answered 10/8, 2009 at 22:37 Comment(4)
Your first answer (in your question) is a million times easier to understand than either of these. That makes it much more pythonic in my book :-/ – Allround
try this: "for a in ntuples(count(), 3): print a;" I think you need to use itertools.tee() to get this to work. – Cyn
count is not an indexable sequence, so it's normal it doesn't work with this method that uses slices extensively... – Casabonne
Hoho, got to love the sarcasm! – Amoroso
H
6

I, as always, like tee:

from itertools import tee, izip, chain

def pairs(iterable):
    a, b = tee(iterable)
    return izip(a, chain(b, [next(b)]))
Hedgepeth answered 27/3, 2012 at 7:32 Comment(0)
C
5

This might be satisfactory:

def pairs(lst):
    for i in range(1, len(lst)):
        yield lst[i-1], lst[i]
    yield lst[-1], lst[0]

>>> a = list(range(5))
>>> for a1, a2 in pairs(a):
...     print a1, a2
...
0 1
1 2
2 3
3 4
4 0

If you like this kind of stuff, look at python articles on wordaligned.org. The author has a special love of generators in python.

Cyn answered 10/8, 2009 at 22:0 Comment(1)
Yeah, I noticed that and I've fixed it -- in the space of time it took to down-vote it. My fault, really. – Cyn
K
2

I'd do it like this (mostly because I can read this):

class Pairs(object):
    def __init__(self, start):
        self.i = start
    def next(self):
        p, p1 = self.i, self.i + 1
        self.i = p1
        return p, p1
    def __iter__(self):
        return self

if __name__ == "__main__":
    x = Pairs(0)
    y = 1
    while y < 20:
        print x.next()
        y += 1

gives:

(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, 5)
(5, 6)
(6, 7)
(7, 8)
(8, 9)
Konyn answered 10/8, 2009 at 23:8 Comment(1)
+1 - Personally, I like this solution much better than the 'yield' based solutions. – Aksum
A
1
[(i,(i+1)%len(range(10))) for i in range(10)]

replace range(10) with the list you want.

In general "circular indexing" is quite easy in python; just use:

a[i%len(a)] 
Abdominous answered 5/6, 2011 at 10:5 Comment(0)
C
0

To answer your question about solving for the general case:

import itertools

def pair(series, n):
    s = list(itertools.tee(series, n))
    try:
        [ s[i].next() for i in range(1, n) for j in range(i)]
    except StopIteration:
        pass
    while True:
        result = []
        try:
            for j, ss in enumerate(s):
                result.append(ss.next())
        except StopIteration:
            if j == 0:
                break
            else:
                s[j] = iter(series)
                for ss in s[j:]:
                    result.append(ss.next())
        yield result

The output is like this:

>>> for a in pair(range(10), 2):
...     print a
...
[0, 1]
[1, 2]
[2, 3]
[3, 4]
[4, 5]
[5, 6]
[6, 7]
[7, 8]
[8, 9]
[9, 0]
>>> for a in pair(range(10), 3):
...     print a
...
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 0]
[9, 0, 1]
Cyn answered 10/8, 2009 at 22:27 Comment(2)
well, pair() is misnamed now ;) – Xanthochroid
Yes, misnamed, not pretty, and 2 minutes after the OP's pythoned-to-the-max self-answer. Ah well. – Cyn
S
0

This infinitely cycles, for good or ill, but is algorithmically very clear.

from itertools import tee, cycle

def nextn(iterable,n=2):
    ''' generator that yields a tuple of the next n items in iterable.
    This generator cycles infinitely '''
    cycled = cycle(iterable)
    gens = tee(cycled,n)

    # advance the iterators, this is O(n^2)
    for (ii,g) in zip(xrange(n),gens):
        for jj in xrange(ii):
            gens[ii].next()

    while True:
        yield tuple([x.next() for x in gens])


def test():
    data = ((range(10),2),
        (range(5),3),
        (list("abcdef"),4),)
    for (iterable, n) in data:
        gen = nextn(iterable,n)
        for j in range(len(iterable)+n):
            print gen.next()            


test()

gives:

(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, 5)
(5, 6)
(6, 7)
(7, 8)
(8, 9)
(9, 0)
(0, 1)
(1, 2)
(0, 1, 2)
(1, 2, 3)
(2, 3, 4)
(3, 4, 0)
(4, 0, 1)
(0, 1, 2)
(1, 2, 3)
(2, 3, 4)
('a', 'b', 'c', 'd')
('b', 'c', 'd', 'e')
('c', 'd', 'e', 'f')
('d', 'e', 'f', 'a')
('e', 'f', 'a', 'b')
('f', 'a', 'b', 'c')
('a', 'b', 'c', 'd')
('b', 'c', 'd', 'e')
('c', 'd', 'e', 'f')
('d', 'e', 'f', 'a')
Seppala answered 11/8, 2009 at 0:42 Comment(2)
I like the use of itertools.cycle(). I am not sure why your test code does range(len(iter) + n). I think range(len(iter)) is correct. Your n**2 code to advance the iterators is like my code: [ s[i].next() for i in range(1, n) for j in range(i)]. – Cyn
re: len(iter)+n... I wanted to show that it really does cycle infinitely. it's just in the test code, after all. Your one-liner for advancing is shorter/better. In any case, that's the one line in the code that definitely needs a comment, since it's the crux of the algorithm :) – Seppala
R
0

Even shorter version of Fortran's zip * range solution (with lambda this time;):

group = lambda t, n: zip(*[t[i::n] for i in range(n)])

group([1, 2, 3, 3], 2)

gives:

[(1, 2), (3, 4)]
Rhodos answered 9/2, 2010 at 19:5 Comment(1)
nice, but not quite right, what I needed was to iterate in pairs, but with repetition of the elements and wrapping, for your example the output should be [(1,2),(2,3),(3,4),(4,1)] – Casabonne
E
0

Here's a version that supports an optional start index (for example to return (4, 0) as the first pair, use start = -1:

import itertools

def iterrot(lst, start = 0):

    if start == 0:
        i = iter(lst)
    elif start > 0:
        i1 = itertools.islice(lst, start, None)
        i2 = itertools.islice(lst, None, start)
        i = itertools.chain(i1, i2)
    else:
        # islice doesn't support negative slice indices so...
        lenl = len(lst)
        i1 = itertools.islice(lst, lenl + start, None)
        i2 = itertools.islice(lst, None, lenl + start)
        i = itertools.chain(i1, i2)
    return i


def iterpairs(lst, start = 0):

    i = iterrot(lst, start)     

    first = prev = i.next()
    for item in i:
        yield prev, item
        prev = item
    yield prev, first


def itertrios(lst, start = 0):

    i = iterrot(lst, start)     

    first = prevprev = i.next()
    second = prev = i.next()
    for item in i:
        yield prevprev, prev, item
        prevprev, prev = prev, item

    yield prevprev, prev, first
    yield prev, first, second
Eustasius answered 10/4, 2011 at 18:44 Comment(0)
A
0
def pairs(ex_list):
    for i, v in enumerate(ex_list):
        if i < len(list) - 1:
            print v, ex_list[i+1]
        else:
            print v, ex_list[0]

Enumerate returns a tuple with the index number and the value. I print the value and the following element of the list ex_list[i+1]. The if i < len(list) - 1 means if v is not the last member of the list. If it is: print v and the first element of the list print v, ex_list[0].

Edit:

You can make it return a list. Just append the printed tuples to a list and return it.

def pairs(ex_list):
    result = []
    for i, v in enumerate(ex_list):
        if i < len(list) - 1:
            result.append((v, ex_list[i+1]))
        else:
            result.append((v, ex_list[0]))
    return result
Aerie answered 15/5, 2013 at 14:13 Comment(1)
Can you explain your answer please ? – Engender
P
0

Of course, you can always use a deque:

from collections import deque
from itertools import *

def pairs(lst, n=2):
    itlst = iter(lst)
    start = list(islice(itlst, 0, n-1))
    deq = deque(start, n)
    for elt in chain(itlst, start):
        deq.append(elt)
        yield list(deq)
Predate answered 19/4, 2014 at 23:7 Comment(0)
S
-1
i=(range(10))

for x in len(i):
    print i[:2]
    i=i[1:]+[i[1]]

more pythonic than this is impossible

Subtangent answered 4/1, 2011 at 1:59 Comment(2)
less pythonic than this is actually very difficult. – Iong
It would help if it actually worked: TypeError: 'int' object is not iterable – Ruling

© 2022 - 2024 β€” McMap. All rights reserved.