Search code examples
pythonpython-itertools

python: how to make a product of iterables without repeating the items?


I need a function that functions in a similar manner as itertools.product, but without repeating items.

For example:

no_repeat_product((1,2,3), (5,6))
= ((1,5), (None,6), (2,5), (None,6), ...(None,6))
no_repeat_product((1,2,3), (5,6), (7,8))
= ((1,5,7), (None,None,8), (None,6,7), (None,None,8), ...(None,None,8))

Any ideas?

Edit: my wording was not correct. I meant without repeating the numbers that are same in successive output values.
For example,

itertools.product((1,2,3), (4,5), (6,7) is
(1,4,6)
(1,4,7), etc  

Here 1,4 appears twice in the output. So, I want to skip writing the numbers when they are the same as the item before. So, the output I want is:

(1,4,6)  
(None,None,7)  

When it is None, it is understood that it is same as its previous item in the result.

Further Edit:

My explanation was still lacking in clarity. Let us assume that I have list of books, chapter numbers, and page numbers. Assume that each book has same number of chapters and each chapter has same number of pages. So, the lists are (book1, book2, book3), (chap1, chap2), (page1, page2, page3).
Now, suppose I want to collect descriptions for each page:
itertools.product will give me:

(book1, chap1, page1), (book1, chap1, page2)..... (book3, chap2, page3)

If I have arranged these pages successively, I do not need to have descriptions repeating. So, if the book and the chapter are the same, in the second page, I don't need to have book and chapter names So, the output should be:

(book1, chap1, page1), (None, None, page2), ..   
(when the pages of first chapter are over..) (None, chap2, page1), (None, None, page2)......  
(when the chapters of the first book are over..)(book2, chap1, page1)..............  
(None, None, page3)  

Solution

  • Based on your comment stating "because (None,None,8) does not occur successively", I'm assuming you only want to None-ify elements that appear in the output immediately before.

    def no_repeat_product(*seq):
        previous = (None,)*len(seq)
        for vals in itertools.product(*seq):
            out = list(vals)
            for i,x in enumerate(out):
                if previous[i] == x:
                    out[i] = None
            previous = vals
            yield(tuple(out))   
    

    Or, if your prefer a more compact and efficient (but less readable) version:

    def no_repeat_product(*seq):
        previous = (None,)*len(seq)
        for vals in itertools.product(*seq):
            out = tuple((y,None)[x==y] for x,y in itertools.izip(previous, vals))
            previous = vals
            yield(out)       
    

    They both do the same thing, and produces the following results:

    for x in no_repeat_product((1,2,3), (5,6), (7,8)): 
        print x 
    

    Output:

    (1, 5, 7)
    (None, None, 8)
    (None, 6, 7)
    (None, None, 8)
    (2, 5, 7)
    (None, None, 8)
    (None, 6, 7)
    (None, None, 8)
    (3, 5, 7)
    (None, None, 8)
    (None, 6, 7)
    (None, None, 8)
    

    For an example in the context of your updated question:

    books = ("Book 1", "Book 2")
    chapters = ("Chapter 1", "Chapter 2")
    pages = ("Page 1", "Page 2", "Page 3")
    
    s1 = max(map(len, books)) + 2  # size of col 1
    s2 = max(map(len, chapters)) + 2  # size of col 2
    x = lambda s, L: (s, "")[s == None].ljust(L)  # Left justify, handle None
    
    for book, chapter, page in no_repeat_product(books, chapters, pages):
        print x(book, s1), x(chapter, s2), page
    

    This gives you:

    Book 1   Chapter 1   Page 1
                         Page 2
                         Page 3
             Chapter 2   Page 1
                         Page 2
                         Page 3
    Book 2   Chapter 1   Page 1
                         Page 2
                         Page 3
             Chapter 2   Page 1
                         Page 2
                         Page 3