วิธี Pythonic ในการรวมสองรายการในแบบสลับกัน?


91

ฉันมีสองรายการแรกที่มีการประกันเพื่อให้มีรายการว่าหนึ่งมากกว่าสอง ฉันต้องการทราบวิธี Pythonic ส่วนใหญ่ในการสร้างรายการใหม่ที่มีค่าดัชนีคู่มาจากรายการแรกและค่าดัชนีคี่มาจากรายการที่สอง

# example inputs
list1 = ['f', 'o', 'o']
list2 = ['hello', 'world']

# desired output
['f', 'hello', 'o', 'world', 'o']

ใช้งานได้ แต่ไม่สวย:

list3 = []
while True:
    try:
        list3.append(list1.pop(0))
        list3.append(list2.pop(0))
    except IndexError:
        break

สิ่งนี้จะทำได้อย่างไร? วิธี Pythonic มากที่สุดคืออะไร?



ไม่ซ้ำ! คำตอบที่ยอมรับในบทความที่เชื่อมโยงด้านบนจะสร้างรายการสิ่งที่รวมกันไม่ใช่รายการเดียว
Paul Sasik

@ พอล: ใช่คำตอบที่ยอมรับไม่ได้ให้คำตอบที่สมบูรณ์ อ่านความคิดเห็นและคำตอบอื่น ๆ คำถามนั้นเหมือนกันโดยทั่วไปและสามารถใช้วิธีแก้ปัญหาอื่น ๆ ได้ที่นี่
Felix Kling

3
@ เฟลิกซ์: ฉันไม่เห็นด้วยอย่างเคารพ เป็นเรื่องจริงคำถามอยู่ในละแวกเดียวกัน แต่ไม่ซ้ำกันจริงๆ ในการพิสูจน์ที่คลุมเครือลองดูคำตอบที่เป็นไปได้ที่นี่และเปรียบเทียบกับคำถามอื่น
Paul Sasik

ลองดูสิ่งเหล่านี้: stackoverflow.com/questions/7529376/…
wordsforthewise

คำตอบ:


119

นี่คือวิธีหนึ่งที่ทำได้โดยการหั่น:

>>> list1 = ['f', 'o', 'o']
>>> list2 = ['hello', 'world']
>>> result = [None]*(len(list1)+len(list2))
>>> result[::2] = list1
>>> result[1::2] = list2
>>> result
['f', 'hello', 'o', 'world', 'o']

3
ขอบคุณ Duncan ฉันไม่รู้ว่าเป็นไปได้ที่จะระบุขั้นตอนเมื่อหั่น สิ่งที่ฉันชอบเกี่ยวกับแนวทางนี้คือการอ่านอย่างเป็นธรรมชาติ 1. ทำรายการความยาวที่ถูกต้อง 2. สร้างดัชนีคู่ที่มีเนื้อหาของรายการ 1 3. เติมดัชนีคี่ที่มีเนื้อหาของรายการ 2 ความจริงที่ว่ารายชื่อมีความยาวต่างกันไม่ใช่ปัญหาในกรณีนี้!
davidchambers

2
ฉันคิดว่ามันใช้ได้เฉพาะเมื่อ len (list1) - len (list2) เป็น 0 หรือ 1
xan

1
หากรายการมีความยาวที่เหมาะสมก็จะใช้งานได้หากไม่เป็นเช่นนั้นคำถามเดิมจะไม่ระบุคำตอบที่คาดหวัง สามารถแก้ไขได้อย่างง่ายดายเพื่อจัดการกับสถานการณ์ที่สมเหตุสมผลที่สุด: ตัวอย่างเช่นหากคุณต้องการให้องค์ประกอบพิเศษถูกละเว้นเพียงแค่ตัดรายการที่ยาวขึ้นก่อนที่คุณจะเริ่ม หากคุณต้องการให้องค์ประกอบพิเศษสอดแทรกด้วย None เพียงแค่ตรวจสอบให้แน่ใจว่าผลลัพธ์นั้นเริ่มต้นด้วย None เพิ่มเติม หากคุณต้องการเพิ่มองค์ประกอบพิเศษในตอนท้ายให้ทำเช่นเดียวกับการเพิกเฉยต่อองค์ประกอบเหล่านั้นแล้วต่อท้าย
Duncan

1
ฉันเองก็ไม่ชัดเจนเช่นกัน ประเด็นที่ฉันพยายามทำคือการแก้ปัญหาของ Duncan ซึ่งแตกต่างจากหลาย ๆ รายการที่ระบุไว้นั้นไม่ซับซ้อนเนื่องจากรายการมีความยาวไม่เท่ากัน แน่นอนว่ามันใช้ได้ในบางสถานการณ์เท่านั้น แต่ฉันต้องการโซลูชันที่หรูหราจริงๆที่ใช้งานได้ในอินสแตนซ์นี้ไปจนถึงโซลูชันที่หรูหราน้อยกว่าซึ่งใช้ได้กับสองรายการ
davidchambers

1
คุณสามารถใช้ (2 * len (list1) -1) แทน (len (list1) + len (list2)) และฉันชอบ [0 :: 2] แทน [:: 2]
Lord British

51

มีสูตรสำหรับสิ่งนี้ในitertoolsเอกสาร :

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).next for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))

แก้ไข:

สำหรับเวอร์ชันของ python ที่มากกว่า 3:

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))

ฉันพบว่าวิธีนี้ซับซ้อนกว่าที่จำเป็น zip_longestมีตัวเลือกที่ดีด้านล่างใช้เป็น
Dubslow

@Dubslow สำหรับกรณีนี้โดยเฉพาะใช่นี่อาจจะมากเกินไป (ตามที่ฉันกล่าวไว้ในความคิดเห็นที่อื่น) เว้นแต่คุณจะสามารถเข้าถึงได้แล้ว มันอาจมีข้อดีบางอย่างในสถานการณ์อื่น ๆ สูตรนี้ไม่ได้ออกแบบมาสำหรับปัญหานี้อย่างแน่นอนมันเกิดขึ้นเพื่อแก้ปัญหานี้
David Z

1
fyi คุณควรใช้สูตรในitertools เอกสารเพราะใช้.next()ไม่ได้อีกต่อไป
จอห์นดับเบิลยู.

1
@johnw. __next__หนึ่งที่มีการใช้ ไม่มีเขียนไว้ในเอกสารดังนั้นฉันจึงเสนอให้แก้ไขคำตอบ
Marine Galantin

@ มารีนฉันอยากจะให้คุณเปลี่ยนตัวอย่างโค้ดที่มีอยู่ แต่ฉันสามารถแก้ไขได้ด้วยตัวเอง ขอบคุณที่ร่วมให้ข้อมูล!
David Z

31

สิ่งนี้ควรทำในสิ่งที่คุณต้องการ:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']

ฉันชอบคำตอบเริ่มต้นของคุณมาก แม้ว่าจะไม่สามารถตอบคำถามได้อย่างสมบูรณ์แบบ แต่ก็เป็นวิธีที่ดีในการรวมสองรายการที่มีความยาวเท่ากัน ฉันขอแนะนำให้รักษาไว้พร้อมกับข้อแม้ความยาวในคำตอบปัจจุบันของคุณ
Paul Sasik

1
หาก list1 เป็น ['f', 'o', 'o', 'd'] แทนรายการสุดท้าย ('d') จะไม่ปรากฏในรายการผลลัพธ์ (ซึ่งเป็นสิ่งที่ดีโดยสิ้นเชิงจากข้อมูลเฉพาะของคำถาม) นี่คือทางออกที่สวยงาม!
davidchambers

1
@Mark ครับ (ผมโหวตให้แล้ว) แค่ชี้ให้เห็นความแตกต่าง (และข้อ จำกัด ถ้าคนอื่นต้องการพฤติกรรมที่แตกต่างกัน)
cobbal

4
+1 สำหรับการแก้ปัญหาที่ระบุไว้และสำหรับการทำมันก็เช่นกัน :-) ฉันคิดว่าสิ่งนี้จะเป็นไปได้ จริงๆแล้วฉันคิดว่าroundrobinฟังก์ชันนี้เกินความจำเป็นสำหรับสถานการณ์นี้
David Z

1
ในการทำงานกับรายการที่มีขนาดใดคุณก็สามารถผนวกสิ่งที่เหลือใน iterators เพื่อผลลัพธ์:list(itertools.chain(map(next, itertools.cycle(iters)), *iters))
แพนด้า-34

30
import itertools
print [x for x in itertools.chain.from_iterable(itertools.izip_longest(list1,list2)) if x]

ฉันคิดว่านี่เป็นวิธีที่ยอดเยี่ยมที่สุดในการทำ


3
เหตุใดจึงไม่เป็นคำตอบที่ยอมรับ นี่คือสิ่งที่สั้นที่สุดและไพ ธ อนิกที่สุดและใช้ได้กับความยาวรายการที่แตกต่างกัน!
Jairo Vadillo

5
ชื่อเมธอดคือ zip_longest ไม่ใช่ izip_longest
Jairo Vadillo

1
ปัญหานี้คือค่าการเติมเริ่มต้นจาก zip_longest อาจเขียนทับNones ที่ * ควรจะอยู่ในรายการ ฉันจะแก้ไขในเวอร์ชันที่ปรับแต่งเพื่อแก้ไข
Dubslow

หมายเหตุ: นี่จะทำให้เกิดปัญหาหากรายการมีองค์ประกอบที่มีค่าFalseหรือสิ่งที่ไม่ว่าจะเป็นเพียงการประเมินเป็นFalseโดยif-expression เช่นตัวอย่างเช่น0หรือรายการที่ว่างเปล่า นี้สามารถเป็นได้ (บางส่วน) [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]หลีกเลี่ยงได้โดยต่อไปนี้: แน่นอนว่าจะยังใช้ไม่ได้หากรายการมีNoneองค์ประกอบที่ต้องเก็บรักษาไว้ ในกรณีนี้คุณต้องเปลี่ยนfillvalueอาร์กิวเมนต์zip_longestตามที่ Dubslow แนะนำไปแล้ว
der_herr_g

Noneดูเหมือนว่าปัญหาจะหายไปอย่างน้อยตั้งแต่ Python 3.7.6 (ฉันไม่รู้สำหรับเวอร์ชันเก่ากว่า) หากalt_chainมีการกำหนดเป็นdef alt_chain(*iters, fillvalue=None): return chain.from_iterable(zip_longest(*iters, fillvalue=fillvalue))แล้วอย่างถูกต้องผลตอบแทนlist(alt_chain([0, False, 1, set(), 3, 4], [0, None, 1, {}], fillvalue=99)) [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99]
paime

18

หากไม่มี itertools และสมมติว่า l1 ยาวกว่า l2 1 รายการ:

>>> sum(zip(l1, l2+[0]), ())[:-1]
('f', 'hello', 'o', 'world', 'o')

ใช้ itertools และสมมติว่ารายการไม่มี None:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')

นี่คือคำตอบที่ชื่นชอบ มันกระชับจัง
mbomb007

@ anishtain4 zip ใช้คู่ขององค์ประกอบเป็น tuples จากรายการ, [(l1[0], l2[0]), (l1[1], l2[1]), ...]. sumรวมสิ่งที่รวมเข้าด้วยกัน: (l1[0], l2[0]) + (l1[1], l2[1]) + ...ทำให้เกิดรายการที่แทรกสลับกัน ส่วนที่เหลือของบรรทัดเดียวเป็นเพียงการเพิ่ม l1 โดยมีองค์ประกอบพิเศษเพื่อให้ซิปทำงานและหั่นเป็นชิ้นจนถึง -1 เพื่อกำจัดช่องว่างนั้น
Zart

izip_longest (zip_longest ตั้งแต่ python 3) ไม่ต้องการ + [0] padding โดยปริยายจะเติม None เมื่อความยาวของรายการไม่ตรงกันในขณะที่filter(None, ...(สามารถใช้boolแทนหรือNone.__ne__) ลบค่าเท็จรวมทั้ง 0, None และสตริงว่าง ดังนั้นนิพจน์ที่สองจึงไม่เทียบเท่ากับนิพจน์แรกอย่างเคร่งครัด
Zart

คำถามคือคุณsumทำได้อย่างไร? อาร์กิวเมนต์ที่สองมีบทบาทอย่างไร startในเอกสารที่อาร์กิวเมนต์ที่สองคือ
anishtain4

ค่าดีฟอลต์ของ start คือ 0 และคุณไม่สามารถทำ 0+ ได้ (บางทูเพิล) ดังนั้น start จึงเปลี่ยนเป็น tuple ว่าง
Zart

13

ฉันรู้ว่าคำถามนั้นถามเกี่ยวกับสองรายการโดยที่รายการหนึ่งมีมากกว่าอีกรายการหนึ่ง แต่ฉันคิดว่าฉันจะใส่สิ่งนี้ให้กับคนอื่น ๆ ที่อาจพบคำถามนี้

นี่คือโซลูชันของ Duncan ที่ปรับให้เข้ากับสองรายการที่มีขนาดแตกต่างกัน

list1 = ['f', 'o', 'o', 'b', 'a', 'r']
list2 = ['hello', 'world']
num = min(len(list1), len(list2))
result = [None]*(num*2)
result[::2] = list1[:num]
result[1::2] = list2[:num]
result.extend(list1[num:])
result.extend(list2[num:])
result

ผลลัพธ์นี้:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 

8

หากทั้งสองรายการมีความยาวเท่ากันคุณสามารถทำได้:

[x for y in zip(list1, list2) for x in y]

เนื่องจากรายการแรกมีอีกหนึ่งองค์ประกอบคุณสามารถเพิ่มโพสต์ hoc:

[x for y in zip(list1, list2) for x in y] + [list1[-1]]

3
^ นี่น่าจะเป็นคำตอบ python กลายเป็น pythonic มากขึ้นในช่วง 10 ปีที่ผ่านมา
Tian

5

นี่คือซับเดียวที่ทำมัน:

list3 = [ item for pair in zip(list1, list2 + [0]) for item in pair][:-1]


2
วิธีนี้ใช้งานได้อย่างถูกต้อง แต่ทำให้ฉันรู้สึกไม่โอ่อ่าเนื่องจากทำหลายอย่างเพื่อให้บรรลุสิ่งที่เรียบง่าย ฉันไม่ได้บอกว่าแนวทางนี้ไม่มีประสิทธิภาพเพียงแค่ว่ามันไม่ง่ายที่จะอ่าน
davidchambers

2
def combine(list1, list2):
    lst = []
    len1 = len(list1)
    len2 = len(list2)

    for index in range( max(len1, len2) ):
        if index+1 <= len1:
            lst += [list1[index]]

        if index+1 <= len2:
            lst += [list2[index]]

    return lst

ระมัดระวังอย่างยิ่งเมื่อใช้อาร์กิวเมนต์เริ่มต้นที่ไม่แน่นอน สิ่งนี้จะส่งคืนคำตอบที่ถูกต้องในครั้งแรกที่เรียกเท่านั้นเนื่องจาก lst จะถูกใช้ซ้ำสำหรับการโทรทุกครั้งหลังจากนั้น ควรเขียนเป็น lst = None จะดีกว่า ... ถ้า lst คือ None: lst = [] แม้ว่าฉันจะไม่เห็นเหตุผลที่น่าสนใจในการเลือกใช้แนวทางนี้กับวิธีอื่น ๆ ที่ระบุไว้ที่นี่
davidchambers

lst ถูกกำหนดภายในฟังก์ชันดังนั้นจึงเป็นตัวแปรท้องถิ่น ปัญหาที่อาจเกิดขึ้นคือ list1 และ list2 จะถูกใช้ซ้ำทุกครั้งที่คุณใช้ฟังก์ชันแม้ว่าคุณจะเรียกใช้ฟังก์ชันด้วยรายการที่แตกต่างกัน ดูdocs.python.org/tutorial/…
blokeley

1
@blokeley: ผิดจะถูกนำมาใช้ใหม่ถ้ารวม (list1 = [... ], list2 = [... ])
killown

เมื่อวิธีนี้ถูกโพสต์ครั้งแรกในการอ่านบรรทัดแรกdef combine(list1, list2, lst=[]):ดังนั้นความคิดเห็นของฉัน ในตอนที่ฉันส่งความคิดเห็นนั้นแม้ว่า killown ได้ทำการเปลี่ยนแปลงที่จำเป็นแล้ว
davidchambers

2

อันนี้ขึ้นอยู่กับการมีส่วนร่วมของ Carlos Valiente ด้านบนโดยมีตัวเลือกในการสลับกลุ่มของหลายรายการและตรวจสอบให้แน่ใจว่ารายการทั้งหมดมีอยู่ในผลลัพธ์:

A=["a","b","c","d"]
B=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]

def cyclemix(xs, ys, n=1):
    for p in range(0,int((len(ys)+len(xs))/n)):
        for g in range(0,min(len(ys),n)):
            yield ys[0]
            ys.append(ys.pop(0))
        for g in range(0,min(len(xs),n)):
            yield xs[0]
            xs.append(xs.pop(0))

print [x for x in cyclemix(A, B, 3)]

สิ่งนี้จะแทรกรายการ A และ B ตามกลุ่ม 3 ค่าแต่ละค่า:

['a', 'b', 'c', 1, 2, 3, 'd', 'a', 'b', 4, 5, 6, 'c', 'd', 'a', 7, 8, 9, 'b', 'c', 'd', 10, 11, 12, 'a', 'b', 'c', 13, 14, 15]

2

อาจจะซื้อช้าหน่อย แต่ python one-liner อีกตัว สิ่งนี้ใช้ได้เมื่อทั้งสองรายการมีขนาดเท่ากันหรือไม่เท่ากัน สิ่งหนึ่งที่ไม่มีค่าคือมันจะปรับเปลี่ยน a และ b หากเป็นปัญหาคุณต้องใช้วิธีแก้ไขปัญหาอื่น

a = ['f', 'o', 'o']
b = ['hello', 'world']
sum([[a.pop(0), b.pop(0)] for i in range(min(len(a), len(b)))],[])+a+b
['f', 'hello', 'o', 'world', 'o']


1

นี่คือซับเดียวโดยใช้การทำความเข้าใจรายการโดยไม่มีไลบรารีอื่น ๆ :

list3 = [sub[i] for i in range(len(list2)) for sub in [list1, list2]] + [list1[-1]]

นี่เป็นอีกแนวทางหนึ่งหากคุณอนุญาตให้แก้ไขรายการเริ่มต้นของคุณโดยผลข้างเคียง:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]


0

หยุดที่สั้นที่สุด:

def interlace(*iters, next = next) -> collections.Iterable:
    """
    interlace(i1, i2, ..., in) -> (
        i1-0, i2-0, ..., in-0,
        i1-1, i2-1, ..., in-1,
        .
        .
        .
        i1-n, i2-n, ..., in-n,
    )
    """
    return map(next, cycle([iter(x) for x in iters]))

แน่นอนว่าการแก้ไขวิธีถัดไป / __ next__ อาจเร็วกว่า


0

สิ่งนี้น่ารังเกียจ แต่ใช้งานได้ไม่ว่ารายการจะมีขนาดเท่าใด:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]

0

หลายบรรทัดเดียวที่ได้รับแรงบันดาลใจจากคำตอบของคำถามอื่น :

import itertools

list(itertools.chain.from_iterable(itertools.izip_longest(list1, list2, fillvalue=object)))[:-1]

[i for l in itertools.izip_longest(list1, list2, fillvalue=object) for i in l if i is not object]

[item for sublist in map(None, list1, list2) for item in sublist][:-1]

0

เป็นไงบ้าง? ใช้งานได้กับสตริงเช่นกัน:

import numpy as np

np.array([[a,b] for a,b in zip([1,2,3],[2,3,4,5,6])]).ravel()

ผลลัพธ์:

array([1, 2, 2, 3, 3, 4])

0

ทางเลือกในการใช้งานและไม่เปลี่ยนรูปแบบ (Python 3):

from itertools import zip_longest
from functools import reduce

reduce(lambda lst, zipped: [*lst, *zipped] if zipped[1] != None else [*lst, zipped[0]], zip_longest(list1, list2),[])

-1

ฉันจะทำง่ายๆ:

chain.from_iterable( izip( list1, list2 ) )

มันจะมาพร้อมกับตัววนซ้ำโดยไม่ต้องสร้างพื้นที่จัดเก็บเพิ่มเติมใด ๆ


1
มันง่ายมาก แต่ใช้ได้กับรายการที่มีความยาวเท่ากันเท่านั้น!
Jochen Ritzel

คุณสามารถแก้ไขได้chain.from_iterable(izip(list1, list2), list1[len(list2):])สำหรับปัญหาเฉพาะที่ถามที่นี่ ... list1 ควรจะยาวกว่านี้
Jochen Ritzel

ใช่ แต่ฉันอยากจะหาวิธีแก้ปัญหาที่ใช้ได้กับภาชนะที่มีความยาวตามอำเภอใจหรือให้ผลกับโซลูชันที่เสนอข้างต้น
วีทตี้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.