简体   繁体   中英

Most clean and efficient way to generate and zip two lists in Python

Given these two lists

zeros = [2,3,1,2]
ones = [3,4,5]

(with the condition that always len(zeros) == len(ones) + 1 )

I want to create one list with alternating 0's and 1's of the magnitude mentioned in the list. I can achieve this by:

zeros_list = [[0]*n for n in zeros]
ones_list = [[1]*n for n in ones]
output = [z for x in zip(zeros_list, ones_list) for y in x for z in y]
output += [0]*zeros[-1]
print(output)
> [0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0]

Is this however the most efficient / clean way? I get a performance of 2.66 µs ± 78.8 ns, but I still have the idea this could be done in a one-liner, and maybe more efficient

Two apparently faster solutions, using the "trick" to special-treat the first zero rather than the last, using it to initialize output .

def superb_rain(zeros, ones):
    zeros = iter(zeros)
    output = [0] * next(zeros)
    for o in ones:
        output += (1,) * o
        output += (0,) * next(zeros)
    return output
def superb_rain2(zeros, ones):
    z = iter(zeros).__next__
    output = [0] * z()
    for o in ones:
        output += (1,) * o
        output += (0,) * z()
    return output

(As @schwobaseggl pointed out, tuples made it about 30% faster.)

Benchmark results:

0.14 us  0.13 us  0.13 us  baseline
3.04 us  3.02 us  2.98 us  original
3.27 us  3.19 us  3.29 us  chepner_1
5.03 us  5.12 us  5.25 us  chepner_2
4.66 us  4.74 us  4.68 us  chepner_2__superb_rain
2.52 us  2.53 us  2.47 us  Alain_T
3.35 us  3.27 us  3.42 us  python_user
1.02 us  0.99 us  1.04 us  superb_rain
1.07 us  1.11 us  1.09 us  superb_rain2

Benchmark code:

import timeit
from itertools import zip_longest, cycle, islice, repeat, chain

def baseline(zeros, ones):
    pass

def original(zeros, ones):
    zeros_list = [[0]*n for n in zeros]
    ones_list = [[1]*n for n in ones]
    output = [z for x in zip(zeros_list, ones_list) for y in x for z in y]
    output += [0]*zeros[-1]
    return output

def chepner_1(zeros, ones):
    return list(chain.from_iterable(chain.from_iterable(zip_longest((repeat(0, x) for x in zeros), (repeat(1, x) for x in ones), fillvalue=[]))))

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            # Remove the iterator we just exhausted from the cycle.
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))
def chepner_2(zeros, ones):
    zero_groups = (repeat(0, x) for x in zeros)
    one_groups = (repeat(1, x) for x in ones)    
    return list(chain.from_iterable(roundrobin(zero_groups, one_groups)))
def chepner_2__superb_rain(zeros, ones):
    return list(chain.from_iterable(map(repeat, cycle([0, 1]), roundrobin(zeros, ones))))

def Alain_T(zeros, ones):
    return [B for N,P in zip(zeros+[0],ones+[0]) for B in ([0]*N+[1]*P)]

def python_user(zeros, ones):
    res = [None] * (len(ones) + len(zeros))

    res[::2] = ([0]*n for n in zeros)
    res[1::2] = ([1]*n for n in ones)

    res = [y for x in res for y in x]
    return res

def superb_rain(zeros, ones):
    zeros = iter(zeros)
    output = [0] * next(zeros)
    for o in ones:
        output += (1,) * o
        output += (0,) * next(zeros)
    return output

def superb_rain2(zeros, ones):
    z = iter(zeros).__next__
    output = [0] * z()
    for o in ones:
        output += (1,) * o
        output += (0,) * z()
    return output

funcs = [
    baseline,
    original,
    chepner_1,
    chepner_2,
    chepner_2__superb_rain,
    Alain_T,
    python_user,
    superb_rain,
    superb_rain2,
    ]

zeros = [2,3,1,2]
ones = [3,4,5]
number = 10**5

expect = original(zeros, ones)
for func in funcs:
    print(func(zeros, ones) == expect, func.__name__)
print()

tss = [[] for _ in funcs]
for _ in range(4):
    for func, ts in zip(funcs, tss):
        t = min(timeit.repeat(lambda: func(zeros, ones), number=number)) / number
        ts.append(t)
        print(*('%.2f us ' % (1e6 * t) for t in ts[1:]), func.__name__)
    print()

Zip with a list comprehension should do the trick.

zeros = [2,3,1,2]
ones = [3,4,5]

output = [B for N,P in zip(zeros,ones+[0]) for B in [0]*N+[1]*P]

print(output)

[0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0]

Note the ones+[0] is to ensure that you don't drop the last value from the zeroes list in the zip operation.

You can use itertools.chain , zip_longest , and itertools.repeat to create a not-too-confusing one-liner.

>>> list(chain.from_iterable(chain.from_iterable(zip_longest((repeat(0, x) for x in zeros), (repeat(1, x) for x in ones), fillvalue=[]))))
[0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0]

On my machine, this took 3.34µs. More importantly, it's the wrapper to list that takes this time. The iterator itself produces the elements on demand, if you don't actually need them all at once.


list(chain.from_iterable(
        chain.from_iterable(zip_longest((repeat(0, x) for x in zeros),
                                        (repeat(1, x) for x in ones),
                                        fill_value=[]))))
  • (repeat(0, x) for x in zeros) creates a sequence of repeat objects that represent your runs of 0s; likewise to create the groups of 1s.
  • zip_longest zips them together into a sequence of pairs, adding a do-nothing empty list to balance the extra value in zeros
  • chain.from_iterable flattens that sequence (from (a, b), (c, d) to (a, b, c, d) .
  • The outer chain.from_iterable then flattens the repeat objects in to a single sequence, which list turns into a list.

You can also simplify the one-liner using the roundrobin recipe from the itertools documentation, which handles both merging the zero groups and one groups, as well as the first round of flattening.

from itertools import cycle, islice, repeat, chain

zeros = [2,3,1,2]
ones = [3,4,5]

# From the itertools documentation
def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            # Remove the iterator we just exhausted from the cycle.
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))


zero_groups = (repeat(0, x) for x in zeros)
one_groups = (repeat(1, x) for x in ones)
    
print(list(chain.from_iterable(roundrobin(zero_groups, one_groups))))

Given these two lists

zeros = [2,3,1,2]
ones = [3,4,5]

(with the condition that always len(zeros) == len(ones) + 1 )

I want to create one list with alternating 0's and 1's of the magnitude mentioned in the list. I can achieve this by:

zeros_list = [[0]*n for n in zeros]
ones_list = [[1]*n for n in ones]
output = [z for x in zip(zeros_list, ones_list) for y in x for z in y]
output += [0]*zeros[-1]
print(output)
> [0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0]

Is this however the most efficient / clean way? I get a performance of 2.66 µs ± 78.8 ns, but I still have the idea this could be done in a one-liner, and maybe more efficient

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM