วิธี "pythonic" มากที่สุดในการวนซ้ำรายการในกลุ่มคืออะไร?


487

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

for i in xrange(0, len(ints), 4):
    # dummy op for example code
    foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]

ดูเหมือนว่า "C-think" มากซึ่งทำให้ฉันสงสัยว่ามีวิธีการรับมือกับสถานการณ์นี้มากขึ้น รายการจะถูกยกเลิกหลังจากทำซ้ำดังนั้นจึงไม่จำเป็นต้องเก็บรักษาไว้ บางทีสิ่งนี้จะดีกว่าไหม?

while ints:
    foo += ints[0] * ints[1] + ints[2] * ints[3]
    ints[0:4] = []

แม้ว่าจะยังไม่ค่อย "รู้สึก" ถูกต้อง : - /

คำถามที่เกี่ยวข้อง: คุณจะแบ่งรายการออกเป็นกลุ่มขนาดเท่า ๆ กันใน Python ได้อย่างไร


3
รหัสของคุณใช้ไม่ได้หากขนาดของรายการไม่เป็นทวีคูณของสี่
Pedro Henriques

5
ฉันขยายรายการไอเอ็นจีเพื่อให้ความยาวมีค่าเป็นทวีคูณของสี่ก่อนที่จะถึงจุดนี้
Ben Blank

4
@ ΤΖΩΤΖΙΟΥ - คำถามมีความคล้ายกันมาก แต่ไม่ซ้ำกันเลยทีเดียว มันคือ "แยกเป็นจำนวนชิ้นขนาดใด ๆ N" เทียบกับ "แยกเป็นชิ้นส่วนขนาดใดก็ได้" :-)
Ben Blank


คำตอบ:


339

ดัดแปลงจากสูตรส่วนของ ธitertoolsเอกสาร:

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

ตัวอย่าง
ใน pseudocode เพื่อเก็บตัวอย่างสั้น

grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'

หมายเหตุ:ในหลาม 2 ใช้แทนizip_longestzip_longest


67
ในที่สุดก็มีโอกาสเล่นกับมันในเซสชั่นหลาม สำหรับผู้ที่สับสนเช่นเดียวกับฉันนี่คือการป้อนตัววนซ้ำเดียวกันกับ izip_longest หลายครั้งทำให้การใช้ค่าต่อเนื่องของลำดับเดียวกันมากกว่าค่าสไทรพ์จากลำดับที่แยกกัน ฉันรักมัน!
Ben Blank

6
วิธีที่ดีที่สุดในการกรอง backvalue คืออะไร ([รายการสำหรับรายการในรายการหากรายการไม่ได้กรอกข้อมูล] สำหรับรายการในปลาเก๋า (iterable))
gotgenes

14
ฉันสงสัยว่าประสิทธิภาพของสูตรปลากะรังสำหรับก้อนขนาด 256k นี้จะแย่มากเพราะizip_longestจะได้รับข้อโต้แย้ง 256k
Anatoly techtonik

13
ในหลาย ๆ ที่ผู้วิจารณ์พูดว่า "เมื่อฉันสรุปว่าสิ่งนี้ได้ผลอย่างไร ... " บางทีอาจจำเป็นต้องใช้คำอธิบายเล็กน้อย รายการของตัววนซ้ำโดยเฉพาะอย่างยิ่ง
LondonRob

6
มีวิธีใช้สิ่งนี้ แต่ไม่มีการNoneเติมก้อนสุดท้ายหรือไม่?
CMCDragonkai

420
def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))
# (in python 2 use xrange() instead of range() to avoid allocating a list)

ง่าย ง่าย. รวดเร็ว ทำงานร่วมกับลำดับใดก็ได้:

text = "I am a very, very helpful text"

for group in chunker(text, 7):
   print repr(group),
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'

print '|'.join(chunker(text, 10))
# I am a ver|y, very he|lpful text

animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']

for group in chunker(animals, 3):
    print group
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']

16
@Carlos รุ่น Crasborn ใช้งานได้สำหรับ iterable ใด ๆ (ไม่เพียงแค่ลำดับเป็นรหัสข้างต้น); มันกระชับและอาจเร็วหรือเร็วกว่า แม้ว่ามันอาจจะคลุมเครือเล็กน้อย (ไม่ชัดเจน) สำหรับคนที่ไม่คุ้นเคยกับitertoolsโมดูล
jfs

1
ตกลง นี่เป็นวิธีที่ธรรมดาที่สุดและ pythonic ชัดเจนและรัดกุม (และทำงานบนแอปกลไก)
Matt Williamson

3
โปรดทราบว่าผลตอบแทนchunker generatorแทนที่ Return เป็น: return [...]เพื่อรับรายการ
Dror

11
แทนการเขียนอาคารฟังก์ชั่นและจากนั้นกลับมาเครื่องกำเนิดไฟฟ้าคุณยังสามารถเขียนเครื่องกำเนิดไฟฟ้าโดยตรงโดยใช้:yield for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]ฉันไม่แน่ใจว่าภายในนี้จะจัดการแตกต่างกันในด้านที่เกี่ยวข้องใด ๆ แต่มันอาจจะชัดเจนขึ้นเล็กน้อย
Alfe

3
โปรดทราบว่านี่ใช้งานได้เฉพาะกับลำดับที่รองรับการเข้าถึงรายการตามดัชนีและจะไม่ทำงานสำหรับตัววนซ้ำทั่วไปเนื่องจากอาจไม่รองรับ__getitem__วิธีการ
apollov

135

ฉันเป็นแฟนของ

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size

มันทำงานอย่างไรถ้า len (ints) ไม่ใช่ chunkSize หลายตัว?
PlsWork

3
@AnnaVopureta chunkจะมีองค์ประกอบ 1, 2 หรือ 3 รายการสำหรับชุดองค์ประกอบสุดท้าย ดูคำถามนี้เกี่ยวกับสาเหตุที่ดัชนีชิ้นสามารถออกจากขอบเขต
Boris

22
import itertools
def chunks(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
    foo += x1 + x2 + x3 + x4

for chunk in chunks(ints,4):
    foo += sum(chunk)

อีกวิธีหนึ่ง:

import itertools
def chunks2(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
    foo += x1 + x2 + x3 + x4

2
+1 สำหรับการใช้เครื่องกำเนิดไฟฟ้าตะเข็บเหมือน "pythonic" ที่มากที่สุดจากโซลูชันที่แนะนำทั้งหมด
Sergey Golovchenko

7
มันค่อนข้างยาวและเงอะงะสำหรับสิ่งที่ง่ายมาก ฉันชอบรุ่นของ S. Lott
zenazn

4
@zenazn: สิ่งนี้จะทำงานกับอินสแตนซ์ของตัวสร้างการแบ่งส่วนจะไม่เกิดขึ้น
Janus Troelsen

นอกเหนือจากการทำงานอย่างถูกต้องกับเครื่องกำเนิดไฟฟ้าและตัววนซ้ำแบบไม่เลื่อนขั้นแรกยังไม่ต้องใช้ค่า "ฟิลเลอร์" หากชิ้นสุดท้ายมีขนาดเล็กกว่าsizeซึ่งบางครั้งเป็นที่ต้องการ
dano

1
+1 สำหรับกำเนิดด้วยเช่นกัน โซลูชันอื่น ๆ ต้องการการlenโทรและดังนั้นจึงไม่สามารถทำงานกับเครื่องกำเนิดไฟฟ้าอื่น
Cuadue


11

ทางออกที่ดีที่สุดสำหรับปัญหานี้ใช้ได้กับตัววนซ้ำ (ไม่ใช่แค่ลำดับ) มันควรจะรวดเร็ว

นี่คือวิธีการแก้ปัญหาที่จัดทำโดยเอกสารประกอบสำหรับ itertools:

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

การใช้ ipython %timeitบนเครื่อง mac book ของฉันทำให้ฉันได้รับ 47.5 ต่อวง

อย่างไรก็ตามสิ่งนี้ไม่ได้ผลสำหรับฉันเพราะผลลัพธ์ที่ได้จะเป็นแบบกลุ่มขนาดใหญ่ วิธีการแก้ปัญหาโดยไม่มีการเติมมีความซับซ้อนมากขึ้นเล็กน้อย ทางออกที่ไร้เดียงสาที่สุดอาจเป็น:

def grouper(size, iterable):
    i = iter(iterable)
    while True:
        out = []
        try:
            for _ in range(size):
                out.append(i.next())
        except StopIteration:
            yield out
            break

        yield out

เรียบง่าย แต่ค่อนข้างช้า: 693 เราต่อลูป

ทางออกที่ดีที่สุดที่ฉันสามารถใช้isliceกับลูปภายใน:

def grouper(size, iterable):
    it = iter(iterable)
    while True:
        group = tuple(itertools.islice(it, None, size))
        if not group:
            break
        yield group

ด้วยชุดข้อมูลเดียวกันฉันได้รับ 305 เราต่อลูป

ไม่สามารถรับโซลูชันที่บริสุทธิ์ได้เร็วกว่านั้นฉันให้คำเตือนที่สำคัญต่อไปนี้: หากข้อมูลอินพุตของคุณมีอินสแตนซ์ของfilldataมันคุณอาจได้รับคำตอบที่ผิด

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    for i in itertools.izip_longest(fillvalue=fillvalue, *args):
        if tuple(i)[-1] == fillvalue:
            yield tuple(v for v in i if v != fillvalue)
        else:
            yield i

ฉันไม่ชอบคำตอบนี้ แต่เร็วกว่ามาก 124 us ต่อลูป


คุณสามารถลดรันไทม์สำหรับสูตร # 3 โดย ~ 10-15% โดยการย้ายไปยังชั้นที่ C (ละเว้นitertoolsการนำเข้า; mapต้อง Py3 mapหรือ):imap def grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n)))))ฟังก์ชั่นสุดท้ายของคุณสามารถทำให้เปราะน้อยลงได้โดยใช้ Sentinel: กำจัดfillvalueอาร์กิวเมนต์ เพิ่มบรรทัดแรกfillvalue = object()แล้วเปลี่ยนifเช็คและสายที่ควบคุมการif i[-1] is fillvalue: yield tuple(v for v in i if v is not fillvalue)รับประกันไม่มีค่าในiterableสามารถเข้าใจผิดว่าค่าฟิลเลอร์
ShadowRanger

BTW ยกนิ้วใหญ่ขึ้น # 4 ฉันกำลังจะโพสต์การเพิ่มประสิทธิภาพของฉัน # 3 เป็นคำตอบที่ดีกว่า (ประสิทธิภาพฉลาด) กว่าสิ่งที่โพสต์จนถึงขณะนี้ แต่ด้วยการปรับแต่งเพื่อให้เชื่อถือได้ # 4 ยืดหยุ่นเร็วกว่าสองเท่าเร็วที่สุด; ฉันไม่ได้คาดหวังวิธีแก้ปัญหาด้วย Python ระดับลูป (และไม่มีความแตกต่างของอัลกอริทึมทางทฤษฎี AFAICT) ที่จะชนะ ฉันถือว่า # 3 แพ้เนื่องจากค่าใช้จ่ายในการสร้าง / วนซ้ำisliceวัตถุ (# 3 ชนะถ้าnค่อนข้างใหญ่เช่นจำนวนกลุ่มมีขนาดเล็ก แต่เป็นการเพิ่มประสิทธิภาพสำหรับกรณีที่ผิดปกติ) แต่ฉันไม่คิดว่ามันจะค่อนข้าง สุดขีด
ShadowRanger

สำหรับ # 4, สาขาแรกของเงื่อนไขจะต้องดำเนินการในการทำซ้ำครั้งล่าสุดเท่านั้น (tuple สุดท้าย) แทนการสร้างมันใหม่ tuple ที่สุดท้ายอีกครั้งแคชโมดูโลของความยาวของ iterable เดิมที่ด้านบนและการใช้งานที่จะฝานปิดช่องว่างที่ไม่พึงประสงค์จากในอันดับสุดท้าย:izip_longest yield i[:modulo]นอกจากนี้สำหรับargsตัวแปร tuple args = (iter(iterable),) * nมันแทนของรายการ: ประหยัดเวลาของนาฬิกาอีกรอบ สุดท้ายถ้าเราไม่สนใจค่าเติมและถือว่าNoneเงื่อนไขสามารถกลายเป็นif None in iรอบสัญญาณนาฬิกามากยิ่งขึ้น
Kumba

1
@ Kumba: ข้อเสนอแนะแรกของคุณจะถือว่าอินพุตมีความยาวเป็นที่รู้จัก หากเป็นตัววนซ้ำ / ตัวสร้างไม่ใช่คอลเล็กชันที่มีความยาวเป็นที่รู้จักจะไม่มีการแคช ไม่มีเหตุผลที่แท้จริงที่จะใช้การเพิ่มประสิทธิภาพเช่นนั้น คุณเพิ่มประสิทธิภาพกรณีที่ผิดปกติ (สุดท้ายyield) ในขณะที่กรณีทั่วไปไม่ได้รับผลกระทบ
ShadowRanger

10

ฉันต้องการวิธีแก้ปัญหาที่จะทำงานกับชุดและเครื่องกำเนิดไฟฟ้า ฉันไม่สามารถเกิดอะไรขึ้นกับสิ่งที่สั้นและสวย แต่มันอ่านได้ค่อนข้างน้อย

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

รายการ:

>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

ตั้ง:

>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

เครื่องกำเนิดไฟฟ้า:

>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

8

คล้ายกับข้อเสนออื่น ๆ แต่ไม่เหมือนกันฉันชอบทำแบบนี้เพราะอ่านง่าย:

it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
    print chunk

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

วิธีนี้คุณจะไม่ได้รับชิ้นส่วนสุดท้าย หากคุณต้องการที่จะได้รับ(9, None, None, None)เป็นก้อนสุดท้ายเพียงใช้จากizip_longestitertools


สามารถปรับปรุงได้ด้วยzip(*([it]*4))
Jean-François Fabre

@ Jean-François Fabre: จากมุมมองที่อ่านง่ายฉันไม่เห็นว่าเป็นการปรับปรุง และก็ช้าลงเล็กน้อย เป็นการปรับปรุงถ้าคุณเล่นกอล์ฟซึ่งฉันไม่ใช่
kriss

ไม่ฉันไม่ได้เล่นกอล์ฟ แต่ถ้าคุณมีข้อโต้แย้ง 10 ข้อ ฉันอ่านสิ่งที่สร้างขึ้นในหน้าอย่างเป็นทางการ แต่แน่นอนฉันไม่สามารถหาได้ในตอนนี้ :)
Jean-François Fabre

@ Jean-François Fabre: ถ้าฉันมี 10 ข้อโต้แย้งหรือจำนวนตัวแปรที่ขัดแย้งกันมันเป็นตัวเลือก แต่ฉันอยากจะเขียน: zip (* (it,) * 10)
kriss

ขวา! นั่นคือสิ่งที่ฉันอ่าน สิ่งที่ไม่รายการที่ผมได้ทำขึ้น :)
Jean-Françoisฟาเบร

8

หากคุณไม่ทราบใช้แพคเกจภายนอกที่คุณสามารถใช้iteration_utilities.grouperจาก1 รองรับทุก iterables (ไม่ใช่แค่ลำดับ):iteration_utilties

from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
    print(group)

ซึ่งพิมพ์:

(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)

ในกรณีที่ความยาวไม่ได้เป็นหลายกลุ่มก็ยังรองรับการเติม (กลุ่มสุดท้ายที่ไม่สมบูรณ์) หรือตัดทอน (ทิ้งกลุ่มสุดท้ายที่ไม่สมบูรณ์) กลุ่มสุดท้าย:

from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)

for group in grouper(seq, 4, fillvalue=None):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)

for group in grouper(seq, 4, truncate=True):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)

มาตรฐาน

ฉันยังตัดสินใจที่จะเปรียบเทียบเวลาทำงานของสองสามวิธีที่กล่าวถึง มันเป็นพล็อตบันทึกการใช้งานที่จัดกลุ่มเป็นกลุ่มขององค์ประกอบ "10" โดยยึดตามรายการขนาดที่แตกต่างกัน สำหรับผลลัพธ์เชิงคุณภาพ: ต่ำกว่าหมายถึงเร็วกว่า:

ป้อนคำอธิบายรูปภาพที่นี่

อย่างน้อยในเกณฑ์มาตรฐานนี้ประสิทธิภาพที่iteration_utilities.grouperดีที่สุด ตามมาด้วยวิธีการของCraz

มาตรฐานถูกสร้างขึ้นด้วย1 รหัสที่ใช้เรียกใช้เบนช์มาร์กนี้คือ:simple_benchmark

import iteration_utilities
import itertools
from itertools import zip_longest

def consume_all(it):
    return iteration_utilities.consume(it, None)

import simple_benchmark
b = simple_benchmark.BenchmarkBuilder()

@b.add_function()
def grouper(l, n):
    return consume_all(iteration_utilities.grouper(l, n))

def Craz_inner(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

@b.add_function()
def Craz(iterable, n, fillvalue=None):
    return consume_all(Craz_inner(iterable, n, fillvalue))

def nosklo_inner(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))

@b.add_function()
def nosklo(seq, size):
    return consume_all(nosklo_inner(seq, size))

def SLott_inner(ints, chunk_size):
    for i in range(0, len(ints), chunk_size):
        yield ints[i:i+chunk_size]

@b.add_function()
def SLott(ints, chunk_size):
    return consume_all(SLott_inner(ints, chunk_size))

def MarkusJarderot1_inner(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot1(iterable,size):
    return consume_all(MarkusJarderot1_inner(iterable,size))

def MarkusJarderot2_inner(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot2(iterable,size):
    return consume_all(MarkusJarderot2_inner(iterable,size))

@b.add_arguments()
def argument_provider():
    for exp in range(2, 20):
        size = 2**exp
        yield size, simple_benchmark.MultiArgument([[0] * size, 10])

r = b.run()

1 Disclaimer: ผมผู้เขียนของห้องสมุดและiteration_utilitiessimple_benchmark


7

เนื่องจากไม่มีใครพูดถึง แต่นี่เป็นzip()วิธีแก้ปัญหา:

>>> def chunker(iterable, chunksize):
...     return zip(*[iter(iterable)]*chunksize)

มันใช้งานได้เฉพาะในกรณีที่ความยาวของลำดับของคุณหารด้วยขนาดของก้อนเสมอหรือคุณไม่สนใจชิ้นส่วนที่ต่อท้ายหากไม่ใช่

ตัวอย่าง:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

หรือใช้itertools.izipเพื่อคืนค่าตัววนซ้ำแทนรายการ:

>>> from itertools import izip
>>> def chunker(iterable, chunksize):
...     return izip(*[iter(iterable)]*chunksize)

Padding สามารถแก้ไขได้โดยใช้คำตอบของ @ ::

>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
...     it   = chain(iterable, repeat(fillvalue, chunksize-1))
...     args = [it] * chunksize
...     return izip(*args)

5

การใช้ map () แทนที่จะใช้ zip () แก้ปัญหาช่องว่างภายในในคำตอบของ JF Sebastian:

>>> def chunker(iterable, chunksize):
...   return map(None,*[iter(iterable)]*chunksize)

ตัวอย่าง:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

2
สิ่งนี้จัดการได้ดีกว่าด้วยitertools.izip_longest(Py2) / itertools.zip_longest(Py3); การใช้งานนี้ไม่ได้รับการสนับสนุนmapเป็นสองเท่าและไม่สามารถใช้ได้ใน Py3 (คุณไม่สามารถผ่านNoneเป็นฟังก์ชั่นตัวทำแผนที่ได้และจะหยุดเมื่อการทำซ้ำสั้นที่สุดหมดลงไม่ใช่ระยะยาวที่สุด
ShadowRanger

4

วิธีการอื่นจะใช้รูปแบบสองอาร์กิวเมนต์ของiter:

from itertools import islice

def group(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

สิ่งนี้สามารถปรับเปลี่ยนให้ใช้ padding ได้ง่าย (คล้ายกับคำตอบของMarkus Jarderot ):

from itertools import islice, chain, repeat

def group_pad(it, size, pad=None):
    it = chain(iter(it), repeat(pad))
    return iter(lambda: tuple(islice(it, size)), (pad,) * size)

สิ่งเหล่านี้สามารถนำมารวมกันเพื่อเสริมช่องว่างภายใน:

_no_pad = object()
def group(it, size, pad=_no_pad):
    if pad == _no_pad:
        it = iter(it)
        sentinel = ()
    else:
        it = chain(iter(it), repeat(pad))
        sentinel = (pad,) * size
    return iter(lambda: tuple(islice(it, size)), sentinel)

1
ดีกว่าเพราะคุณมีตัวเลือกที่จะไม่เว้นช่องว่างภายใน!
n611x007

3

หากรายการมีขนาดใหญ่วิธีที่มีประสิทธิภาพสูงสุดในการทำเช่นนี้คือการใช้ตัวสร้าง:

def get_chunk(iterable, chunk_size):
    result = []
    for item in iterable:
        result.append(item)
        if len(result) == chunk_size:
            yield tuple(result)
            result = []
    if len(result) > 0:
        yield tuple(result)

for x in get_chunk([1,2,3,4,5,6,7,8,9,10], 3):
    print x

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

(ผมคิดว่าข้อเสนอแนะ itertools MizardX เป็นหน้าที่เทียบเท่านี้.)
โรเบิร์ต Rossney

1
(อันที่จริงการสะท้อนไม่มีฉันทำไม่ได้ itertools.islice กลับ iterator แต่มันไม่ได้ใช้อย่างใดอย่างหนึ่งที่มีอยู่..)
โรเบิร์ต Rossney

มันดีและเรียบง่าย แต่ด้วยเหตุผลบางอย่างแม้ว่าจะไม่มีการแปลงเป็นทูเปิล 4-7 เท่าช้ากว่าวิธีการยอมรับของปลาเก๋าในiterable = range(100000000)& chunksizeถึง 10,000
Valentas

อย่างไรก็ตามโดยทั่วไปแล้วฉันอยากจะแนะนำวิธีนี้เนื่องจากวิธีที่ได้รับการยอมรับอาจช้ามากเมื่อการตรวจสอบรายการสุดท้ายช้าdocs.python.org/3/library/itertools.html#itertools.zip_longest
Valentas

3

การใช้ฟังก์ชั่นเล็ก ๆ น้อย ๆ และสิ่งต่าง ๆ ไม่ได้ดึงดูดใจฉันเลย ฉันชอบที่จะใช้ชิ้น:

data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
    ...

ดี lenแต่ไม่ดีสำหรับกระแสที่ไม่แน่นอนซึ่งได้รู้จักกันไม่มี คุณสามารถทำแบบทดสอบด้วยหรือitertools.repeat itertools.cycle
n611x007

1
นอกจากนี้กินหน่วยความจำเนื่องจากการใช้[...for...] list comprehensionเพื่อสร้างรายการแทนการใช้(...for...) expression expressionซึ่งจะสนใจองค์ประกอบถัดไปและหน่วยความจำสำรอง
n611x007

2

วิธีหลีกเลี่ยงการแปลงทั้งหมดในรายการimport itertoolsและ:

>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
...     list(g)

ผลิต:

... 
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>> 

ฉันตรวจสอบgroupbyแล้วและไม่ได้แปลงเป็นรายการหรือใช้lenดังนั้นฉัน (คิดว่า) นี่จะชะลอความละเอียดของแต่ละค่าจนกว่าจะใช้จริง น่าเสียดายที่ไม่มีคำตอบที่พร้อมใช้งาน (ในเวลานี้) ดูเหมือนจะเสนอรูปแบบนี้

เห็นได้ชัดว่าถ้าคุณจำเป็นต้องจัดการแต่ละรายการในทางกลับกันรังสำหรับห่วงมากกว่า g:

for k,g in itertools.groupby(xrange(35), lambda x: x/10):
    for i in g:
       # do what you need to do with individual items
    # now do what you need to do with the whole group

ความสนใจเฉพาะของฉันเกี่ยวกับเรื่องนี้คือต้องใช้ตัวสร้างเพื่อส่งการเปลี่ยนแปลงเป็นกลุ่มมากถึง 1,000 ถึง gmail API:

    messages = a_generator_which_would_not_be_smart_as_a_list
    for idx, batch in groupby(messages, lambda x: x/1000):
        batch_request = BatchHttpRequest()
        for message in batch:
            batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
        http = httplib2.Http()
        self.credentials.authorize(http)
        batch_request.execute(http=http)

เกิดอะไรขึ้นถ้ารายการที่คุณกำลัง chunking เป็นสิ่งอื่นที่ไม่ใช่ลำดับของจำนวนเต็มน้อยไปหามาก?
PaulMcG

@PaulMcGuire เห็นgroupby ; กำหนดฟังก์ชั่นเพื่ออธิบายลำดับจากนั้นองค์ประกอบของ iterable สามารถเป็นอะไรก็ได้จริงไหม?
John Mee

1
ใช่ฉันคุ้นเคยกับกลุ่มโดย แต่ถ้าข้อความเป็นตัวอักษร "ABCDEFG" คุณก็groupby(messages, lambda x: x/3)จะได้ TypeError (สำหรับการพยายามแบ่งสตริงด้วย int) ไม่ใช่การจัดกลุ่ม 3 ตัวอักษร ตอนนี้ถ้าคุณทำgroupby(enumerate(messages), lambda x: x[0]/3)คุณอาจมีบางอย่าง แต่คุณไม่ได้พูดอย่างนั้นในโพสต์ของคุณ
PaulMcG

2

ด้วย NumPy มันง่าย:

ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
    print(int1, int2)

เอาท์พุท:

1 2
3 4
5 6
7 8

2
def chunker(iterable, n):
    """Yield iterable in chunk sizes.

    >>> chunks = chunker('ABCDEF', n=4)
    >>> chunks.next()
    ['A', 'B', 'C', 'D']
    >>> chunks.next()
    ['E', 'F']
    """
    it = iter(iterable)
    while True:
        chunk = []
        for i in range(n):
            try:
                chunk.append(next(it))
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()

2

ถ้าฉันไม่ได้ทำอะไรเลยโซลูชันที่เรียบง่ายต่อไปนี้ที่มีนิพจน์ตัวสร้างไม่ได้ถูกกล่าวถึง มันถือว่าทั้งขนาดและจำนวนชิ้นเป็นที่รู้จัก (ซึ่งมักเป็นกรณี) และไม่จำเป็นต้องมีการขยาย:

def chunks(it, n, m):
    """Make an iterator over m first chunks of size n.
    """
    it = iter(it)
    # Chunks are presented as tuples.
    return (tuple(next(it) for _ in range(n)) for _ in range(m))

1

ในวิธีที่สองของคุณฉันจะไปยังกลุ่มถัดไปของ 4 โดยทำสิ่งนี้:

ints = ints[4:]

อย่างไรก็ตามฉันยังไม่ได้ทำการวัดประสิทธิภาพใด ๆ ดังนั้นฉันจึงไม่รู้ว่าอันไหนที่อาจมีประสิทธิภาพมากกว่า

ต้องบอกว่าฉันมักจะเลือกวิธีแรก มันไม่สวย แต่มักเป็นผลมาจากการเชื่อมต่อกับโลกภายนอก


1

อีกคำตอบข้อดีคือ:

1) เข้าใจได้ง่าย
2) ทำงานบน iterable ใด ๆ ไม่ใช่แค่ลำดับ (คำตอบข้างต้นบางอย่างจะทำให้หายใจไม่ออกบน filehandles)
3) ไม่โหลด chunk ลงในหน่วยความจำทั้งหมดในครั้งเดียว
4) ไม่สร้างรายการอ้างอิงยาว ๆ ตัววนซ้ำเดียวกันในหน่วยความจำ
5) ไม่มีการเติมเต็มของการเติมค่าในตอนท้ายของรายการ

ที่ถูกกล่าวว่าฉันไม่ได้กำหนดเวลาดังนั้นจึงอาจช้ากว่าวิธีที่ฉลาดกว่าและข้อดีบางประการอาจไม่เกี่ยวข้องกับกรณีการใช้งาน

def chunkiter(iterable, size):
  def inneriter(first, iterator, size):
    yield first
    for _ in xrange(size - 1): 
      yield iterator.next()
  it = iter(iterable)
  while True:
    yield inneriter(it.next(), it, size)

In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:                                                
          for c in ii:
            print c,
          print ''
        ...:     
        a b c 
        d e f 
        g h 

อัปเดต:
ข้อเสียสองประการเนื่องจากลูปด้านในและด้านนอกดึงค่าจากตัววนซ้ำเดียวกัน:
1) การดำเนินการต่อไม่ทำงานตามที่คาดไว้ในลูปด้านนอก - เพียงแค่ดำเนินการต่อในรายการถัดไปแทนที่จะข้ามก้อน . อย่างไรก็ตามนี่ดูเหมือนจะไม่เป็นปัญหาเพราะไม่มีอะไรให้ทดสอบในลูปภายนอก
2) ตัวแบ่งไม่ทำงานตามที่คาดไว้ในวงใน - การควบคุมจะวนกลับในวงในอีกครั้งด้วยรายการถัดไปในตัววนซ้ำ หากต้องการข้ามชิ้นส่วนทั้งห่อตัวทำซ้ำด้านใน (ii ด้านบน) ใน tuple เช่นfor c in tuple(ii)หรือตั้งค่าสถานะและปล่อย iterator


1
def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

+1 มันละเว้นการแพ็ด; คุณและ bcoughlan 'sจะคล้ายกันมาก
n611x007

1

คุณสามารถใช้ฟังก์ชันpartitionหรือchunksจากfuncy library:

from funcy import partition

for a, b, c, d in partition(4, ints):
    foo += a * b * c * d

ฟังก์ชั่นเหล่านี้ยังมีรุ่นตัววนซ้ำipartitionและichunksซึ่งจะมีประสิทธิภาพมากขึ้นในกรณีนี้

นอกจากนี้คุณยังสามารถมองที่การดำเนินงานของพวกเขา


1

เกี่ยวกับการแก้ปัญหาโดยJ.F. Sebastian ที่นี่ :

def chunker(iterable, chunksize):
    return zip(*[iter(iterable)]*chunksize)

มันฉลาด แต่มีข้อเสียอย่างเดียว - คืนค่า tuple ทุกครั้ง วิธีรับสตริงแทน
แน่นอนคุณสามารถเขียน''.join(chunker(...))ได้ แต่ tuple ชั่วคราวถูกสร้างขึ้นแล้ว

คุณสามารถกำจัด tuple ชั่วคราวด้วยการเขียนของตัวเองzipเช่นนี้:

class IteratorExhausted(Exception):
    pass

def translate_StopIteration(iterable, to=IteratorExhausted):
    for i in iterable:
        yield i
    raise to # StopIteration would get ignored because this is generator,
             # but custom exception can leave the generator.

def custom_zip(*iterables, reductor=tuple):
    iterators = tuple(map(translate_StopIteration, iterables))
    while True:
        try:
            yield reductor(next(i) for i in iterators)
        except IteratorExhausted: # when any of iterators get exhausted.
            break

แล้วก็

def chunker(data, size, reductor=tuple):
    return custom_zip(*[iter(data)]*size, reductor=reductor)

ตัวอย่างการใช้งาน:

>>> for i in chunker('12345', 2):
...     print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
...     print(repr(i))
...
'12'
'34'

2
ไม่ใช่คำวิจารณ์ที่มีไว้สำหรับคุณในการเปลี่ยนคำตอบ แต่เป็นความเห็น: รหัสเป็นความรับผิดชอบ ยิ่งคุณเขียนรหัสมากเท่าไหร่คุณก็ยิ่งสร้างพื้นที่สำหรับบั๊กที่จะซ่อน จากมุมมองนี้การเขียนใหม่zipแทนที่จะใช้สิ่งที่มีอยู่ดูเหมือนจะไม่ใช่ความคิดที่ดีที่สุด
Alfe

1

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

def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
    chunk = tuple(next(it) for _ in range(chunk_size))
    if not chunk:
        break
    yield chunk

1

ฉันไม่เคยต้องการเบาะรองนั่งเพื่อให้ความต้องการนั้นเป็นสิ่งจำเป็น ฉันพบว่าความสามารถในการทำงานกับ iterable ใด ๆ ก็เป็นข้อกำหนดเช่นกัน ระบุว่าผมตัดสินใจที่จะขยายในคำตอบที่ได้รับการยอมรับ, https://stackoverflow.com/a/434411/1074659

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

#!/usr/bin/env python3
from itertools import zip_longest


_UNDEFINED = object()


def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
    """
    Collect data into chunks and optionally pad it.

    Performance worsens as `chunksize` approaches 1.

    Inspired by:
        https://docs.python.org/3/library/itertools.html#itertools-recipes

    """
    args = [iter(iterable)] * chunksize
    chunks = zip_longest(*args, fillvalue=fillvalue)
    yield from (
        filter(lambda val: val is not _UNDEFINED, chunk)
        if chunk[-1] is _UNDEFINED
        else chunk
        for chunk in chunks
    ) if fillvalue is _UNDEFINED else chunks

1

นี่คือ chunker ที่ไม่มีการนำเข้าที่รองรับเครื่องกำเนิดไฟฟ้า:

def chunks(seq, size):
    it = iter(seq)
    while True:
        ret = tuple(next(it) for _ in range(size))
        if len(ret) == size:
            yield ret
        else:
            raise StopIteration()

ตัวอย่างการใช้งาน:

>>> def foo():
...     i = 0
...     while True:
...         i += 1
...         yield i
...
>>> c = chunks(foo(), 3)
>>> c.next()
(1, 2, 3)
>>> c.next()
(4, 5, 6)
>>> list(chunks('abcdefg', 2))
[('a', 'b'), ('c', 'd'), ('e', 'f')]

1

กับงูหลาม 3.8 itertools.isliceคุณสามารถใช้ประกอบการวอลรัสและ

from itertools import islice

list_ = [i for i in range(10, 100)]

def chunker(it, size):
    iterator = iter(it)
    while chunk := list(islice(iterator, size)):
        print(chunk)
In [2]: chunker(list_, 10)                                                         
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69]
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

0

ดูเหมือนจะไม่มีวิธีที่ดีในการทำเช่นนี้ นี่คือหน้าที่มีหลายวิธี ได้แก่ :

def split_seq(seq, size):
    newseq = []
    splitsize = 1.0/size*len(seq)
    for i in range(size):
        newseq.append(seq[int(round(i*splitsize)):int(round((i+1)*splitsize))])
    return newseq

0

หากรายการมีขนาดเดียวกันคุณสามารถรวมไว้ในรายชื่อของ 4 tuples zip()กับ ตัวอย่างเช่น:

# Four lists of four elements each.

l1 = range(0, 4)
l2 = range(4, 8)
l3 = range(8, 12)
l4 = range(12, 16)

for i1, i2, i3, i4 in zip(l1, l2, l3, l4):
    ...

นี่คือสิ่งที่zip()ฟังก์ชั่นผลิต:

>>> print l1
[0, 1, 2, 3]
>>> print l2
[4, 5, 6, 7]
>>> print l3
[8, 9, 10, 11]
>>> print l4
[12, 13, 14, 15]
>>> print zip(l1, l2, l3, l4)
[(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]

หากรายการมีขนาดใหญ่และคุณไม่ต้องการรวมไว้ในรายการที่ใหญ่กว่าให้ใช้itertools.izip()ซึ่งสร้างตัววนซ้ำแทนที่จะเป็นรายการ

from itertools import izip

for i1, i2, i3, i4 in izip(l1, l2, l3, l4):
    ...

0

หนึ่งซับแก้ปัญหา adhoc เพื่อย้ำกว่ารายการxในกลุ่มของขนาด4-

for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
    ... do something with a, b, c and d ...
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.