รับจำนวนองค์ประกอบในตัววนซ้ำใน Python


142

มีวิธีที่มีประสิทธิภาพในการทราบจำนวนองค์ประกอบที่อยู่ในตัววนซ้ำใน Python โดยทั่วไปโดยไม่ต้องวนซ้ำในแต่ละรายการและนับ


คำตอบ:


104

ไม่เป็นไปไม่ได้

ตัวอย่าง:

import random

def gen(n):
    for i in xrange(n):
        if random.randint(0, 1) == 0:
            yield i

iterator = gen(10)

iteratorไม่ทราบความยาวของจนกว่าคุณจะวนซ้ำ


14
อีกวิธีหนึ่งdef gen(): yield random.randint(0, 1)คือไม่มีที่สิ้นสุดดังนั้นคุณจะไม่สามารถหาความยาวได้โดยการวนซ้ำ
tgray

1
ดังนั้นเพื่อตรวจสอบความถูกต้องที่ชัดเจน: วิธีที่ดีที่สุดในการรับ "ขนาด" ของตัววนซ้ำคือการนับจำนวนครั้งที่คุณทำซ้ำไปแล้วใช่ไหม ในกรณีนี้จะเป็นnumIters = 0 ; while iterator: numIters +=1อย่างไร
Mike Williamson

น่าสนใจจึงเป็นปัญหาที่หยุดชะงัก
Akababa

241

รหัสนี้ควรใช้งานได้:

>>> iter = (i for i in range(50))
>>> sum(1 for _ in iter)
50

แม้ว่าจะวนซ้ำในแต่ละรายการและนับเป็นวิธีที่เร็วที่สุดในการทำเช่นนั้น

นอกจากนี้ยังใช้งานได้เมื่อตัววนซ้ำไม่มีรายการ:

>>> sum(1 for _ in range(0))
0

แน่นอนว่ามันทำงานตลอดไปสำหรับอินพุตที่ไม่มีที่สิ้นสุดดังนั้นโปรดจำไว้ว่าตัววนซ้ำสามารถไม่มีที่สิ้นสุด:

>>> sum(1 for _ in itertools.count())
[nothing happens, forever]

นอกจากนี้ทราบว่าiterator จะหมดโดยการทำเช่นนี้และความพยายามที่จะใช้มันจะเห็นองค์ประกอบไม่มี นั่นเป็นผลที่หลีกเลี่ยงไม่ได้จากการออกแบบตัววนซ้ำ Python หากคุณต้องการเก็บองค์ประกอบไว้คุณจะต้องเก็บไว้ในรายการหรือบางอย่าง


11
สำหรับฉันดูเหมือนว่านี่จะทำในสิ่งที่ OP ไม่ต้องการทำ: วนซ้ำผ่านตัววนซ้ำและนับ
Adam Crossland

37
นี่เป็นวิธีที่ประหยัดพื้นที่ในการนับองค์ประกอบในการทำซ้ำ
Captain Lepton

9
แม้ว่านี่จะไม่ใช่สิ่งที่ OP ต้องการเนื่องจากคำถามของเขาไม่มีคำตอบ แต่คำตอบนี้จะหลีกเลี่ยงการสร้างอินสแตนซ์ของรายการและจะเร็วกว่าค่าคงที่ในเชิงประจักษ์โดยใช้วิธีลดที่ระบุไว้ข้างต้น
Phillip Nordwall

5
ช่วยไม่ได้: มีการ_อ้างอิงถึง Perl $_หรือไม่? :)
Alois Mahdal

17
@AloisMahdal ไม่เป็นเรื่องธรรมดาใน Python ที่จะใช้ชื่อ_สำหรับตัวแปรดัมมี่ที่คุณไม่สนใจ
Taymon

69

ไม่วิธีการใด ๆ ที่จะทำให้คุณต้องแก้ไขทุกผลลัพธ์ คุณทำได้

iter_length = len(list(iterable))

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

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

แก้ไข: การใช้list()จะอ่านซ้ำทั้งหมดลงในหน่วยความจำในครั้งเดียวซึ่งอาจไม่เป็นที่ต้องการ อีกวิธีหนึ่งคือการทำ

sum(1 for _ in iterable)

ตามที่บุคคลอื่นโพสต์ นั่นจะหลีกเลี่ยงไม่ให้มันอยู่ในความทรงจำ


ปัญหาคือฉันกำลังอ่านไฟล์ที่มี "pysam" ซึ่งมีหลายล้านรายการ Pysam ส่งคืนตัวทำซ้ำ ในการคำนวณปริมาณที่แน่นอนฉันจำเป็นต้องรู้ว่ามีการอ่านกี่ครั้งในไฟล์ แต่ฉันไม่จำเป็นต้องอ่านแต่ละครั้ง ... นั่นคือปัญหา

6
ฉันไม่ใช่ผู้ใช้ pysam แต่อาจกำลังอ่านไฟล์ "ขี้เกียจ" มันสมเหตุสมผลเพราะคุณไม่ต้องการมีไฟล์ขนาดใหญ่ในหน่วยความจำ ดังนั้นถ้าคุณต้องรู้ว่าไม่ ของเร็กคอร์ดก่อนการทำซ้ำวิธีเดียวคือสร้างตัวทำซ้ำสองตัวและใช้ตัวแรกเพื่อนับองค์ประกอบและตัวที่สองเพื่ออ่านไฟล์ BTW. อย่าใช้len(list(iterable))มันจะโหลดข้อมูลทั้งหมดไปยังหน่วยความจำ คุณสามารถใช้: reduce(lambda x, _: x+1, iterable, 0). แก้ไข: รหัส Zonda333 พร้อมผลรวมก็ดีเช่นกัน
Tomasz Wysocki

1
@ user248237: ทำไมคุณถึงบอกว่าคุณต้องรู้ว่ามีกี่รายการเพื่อคำนวณปริมาณที่แน่นอน คุณสามารถอ่านจำนวนคงที่ของพวกเขาและจัดการกรณีเมื่อมีน้อยกว่าจำนวนคงที่ (ทำได้ง่ายมากโดยใช้ iterslice) มีเหตุผลอื่นที่คุณต้องอ่านรายการทั้งหมดหรือไม่?
kriss

1
@Tomasz โปรดทราบว่าการลดจะเลิกใช้แล้วและจะหายไปใน Python 3 ขึ้นไป
Wilduck

7
@ วิลดูค: ยังไม่ไปเพิ่งย้ายไปfunctools.reduce
Daenyth

34

คุณทำไม่ได้ (ยกเว้นประเภทของตัววนซ้ำเฉพาะที่ใช้วิธีการเฉพาะบางอย่างที่ทำให้เป็นไปได้)

โดยทั่วไปคุณสามารถนับรายการวนซ้ำได้โดยการใช้ตัววนซ้ำเท่านั้น หนึ่งในวิธีที่มีประสิทธิภาพมากที่สุด:

import itertools
from collections import deque

def count_iter_items(iterable):
    """
    Consume an iterable not reading it into memory; return the number of items.
    """
    counter = itertools.count()
    deque(itertools.izip(iterable, counter), maxlen=0)  # (consume at C speed)
    return next(counter)

(สำหรับ Python 3.x แทนที่itertools.izipด้วยzip)


3
+1: เมื่อเปรียบเทียบกับเวลาsum(1 for _ in iterator)นี้เร็วกว่าเกือบสองเท่า
augustomen

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

สิ่งสำคัญที่ควรทราบ (ซึ่งฉันมองข้ามไป) ลำดับของการโต้แย้งมีความzipสำคัญ : ถ้าคุณผ่านzip(counter, iterable)จริงคุณจะได้รับ 1 มากกว่าจำนวนที่ซ้ำได้!
Kye W Shi

คำตอบที่ดีมาก จะให้รางวัลกับมัน
Reut Sharabani

18

ใจดี. คุณสามารถตรวจสอบ__length_hint__วิธีการได้ แต่ขอเตือนว่า (อย่างน้อยก็ถึง Python 3.4 ตามที่ gsnedders ชี้ให้เห็นอย่างเป็นประโยชน์) เป็นรายละเอียดการใช้งานที่ไม่มีเอกสาร ( ข้อความต่อไปนี้ในเธรด ) ซึ่งอาจหายไปหรือเรียกปีศาจจมูกแทนได้

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


5
ไม่เป็นเช่นนั้นอีกต่อไปเช่นเดียวกับPEP 424และ Python 3.4 __length_hint__ขณะนี้ได้รับการจัดทำเป็นเอกสารแล้ว แต่เป็นคำใบ้และไม่รับประกันความถูกต้อง
gsnedders

13

ดังนั้นสำหรับผู้ที่ต้องการทราบข้อมูลสรุปของการสนทนานั้น คะแนนสูงสุดสุดท้ายสำหรับการนับนิพจน์เครื่องกำเนิดไฟฟ้าที่มีความยาว 50 ล้านโดยใช้:

  • len(list(gen)),
  • len([_ for _ in gen]),
  • sum(1 for _ in gen),
  • ilen(gen)(จากmore_itertool ),
  • reduce(lambda c, i: c + 1, gen, 0),

จัดเรียงตามประสิทธิภาพของการดำเนินการ (รวมถึงการใช้หน่วยความจำ) จะทำให้คุณประหลาดใจ:

``

1: test_list.py:8: 0.492 KiB

gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))

('รายการวินาที', 1.9684218849870376)

2: test_list_compr.py:8: 0.867 KiB

gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])

('list_compr วินาที', 2.5885991149989422)

3: test_sum.py:8: 0.859 KiB

gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()

('ผลรวมวินาที', 3.441088170016883)

4: more_itertools / more.py: 413: 1.266 KiB

d = deque(enumerate(iterable, 1), maxlen=1)

test_ilen.py:10: 0.875 KiB
gen = (i for i in data*1000); t0 = monotonic(); ilen(gen)

('ilen, วินาที', 9.812256851990242)

5: test_reduce.py:8: 0.859 KiB

gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)

('ลดวินาที', 13.436614598002052) "

ดังนั้นจึงlen(list(gen))เป็นหน่วยความจำที่ใช้บ่อยที่สุดและใช้งานน้อยที่สุด


คุณวัดการใช้หน่วยความจำได้อย่างไร?
normanius

2
คุณสามารถอธิบายได้ไหมว่าทำไมจึงlen(list(gen))ควรใช้หน่วยความจำน้อยกว่าวิธีการลด อดีตสร้างใหม่listที่เกี่ยวข้องกับการจัดสรรหน่วยความจำในขณะที่หลังไม่ควร ดังนั้นฉันคาดหวังว่าหน่วยความจำรุ่นหลังจะมีประสิทธิภาพมากขึ้น นอกจากนี้การใช้หน่วยความจำจะขึ้นอยู่กับประเภทองค์ประกอบ
normanius

FYI: ฉันสามารถสร้างซ้ำสำหรับ python 3.6.8 (บน MacBookPro) ซึ่งวิธีที่ 1 มีประสิทธิภาพดีกว่าวิธีอื่น ๆ ในแง่ของรันไทม์ (ฉันข้ามวิธีที่ 4)
normanius

len(tuple(iterable))ได้อย่างมีประสิทธิภาพมากขึ้น: บทความโดย Nelson Minar
VMAtm

12

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

การใช้งาน:

>>> import cardinality
>>> cardinality.count([1, 2, 3])
3
>>> cardinality.count(i for i in range(500))
500
>>> def gen():
...     yield 'hello'
...     yield 'world'
>>> cardinality.count(gen())
2

count()การนำไปใช้งานจริงมีดังนี้:

def count(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

ฉันคิดว่าคุณยังคงสามารถทำซ้ำบนตัววนซ้ำได้หากคุณใช้ฟังก์ชันนั้นใช่ไหม
jcollum

9

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


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

1
@ ทอมฉันใช้ LinkedList เป็นตัวอย่างส่วนใหญ่คุณไม่รู้ว่าคุณมีเท่าไหร่เพราะคุณรู้แค่ว่ามีอะไรเกิดขึ้นต่อไป (ถ้ามีบางอย่าง) ฉันต้องขออภัยหากถ้อยคำของฉันดูขัดหูขัดตาหรือส่อว่าเป็นหนึ่งเดียวกัน
Jesus Ramos

8

สำหรับคำถามเดิมของคุณคำตอบก็คือโดยทั่วไปไม่มีวิธีใดที่จะทราบความยาวของตัววนซ้ำใน Python

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


6

เกณฑ์มาตรฐานอย่างรวดเร็ว:

import collections
import itertools

def count_iter_items(iterable):
    counter = itertools.count()
    collections.deque(itertools.izip(iterable, counter), maxlen=0)
    return next(counter)

def count_lencheck(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

def count_sum(iterable):           
    return sum(1 for _ in iterable)

iter = lambda y: (x for x in xrange(y))

%timeit count_iter_items(iter(1000))
%timeit count_lencheck(iter(1000))
%timeit count_sum(iter(1000))

ผลลัพธ์:

10000 loops, best of 3: 37.2 µs per loop
10000 loops, best of 3: 47.6 µs per loop
10000 loops, best of 3: 61 µs per loop

เช่น count_iter_items อย่างง่ายเป็นวิธีที่จะไป

การปรับสิ่งนี้สำหรับ python3:

61.9 µs ± 275 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
74.4 µs ± 190 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
82.6 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

หมายเหตุ: การทดสอบนี้ใช้ python2
normanius

3

มีสองวิธีในการรับความยาวของ "บางสิ่ง" บนคอมพิวเตอร์

วิธีแรกคือการจัดเก็บการนับ - สิ่งนี้ต้องการสิ่งใดก็ตามที่สัมผัสกับไฟล์ / ข้อมูลเพื่อแก้ไข (หรือคลาสที่เปิดเผยเฉพาะอินเทอร์เฟซ - แต่มันกลับมาเหมือนเดิม)

อีกวิธีหนึ่งคือการทำซ้ำและนับว่ามันใหญ่แค่ไหน


0

เป็นเรื่องปกติที่จะใส่ข้อมูลประเภทนี้ในส่วนหัวของไฟล์และสำหรับ pysam จะให้คุณเข้าถึงข้อมูลนี้ได้ ฉันไม่ทราบรูปแบบ แต่คุณได้ตรวจสอบ API แล้วหรือยัง

อย่างที่คนอื่นบอกคุณไม่สามารถทราบความยาวจากตัววนซ้ำได้


0

สิ่งนี้ขัดกับคำจำกัดความของตัววนซ้ำซึ่งเป็นตัวชี้ไปยังวัตถุรวมทั้งข้อมูลเกี่ยวกับวิธีไปยังวัตถุถัดไป

ตัววนซ้ำไม่รู้ว่าจะสามารถทำซ้ำได้อีกกี่ครั้งจนกว่าจะยุติ สิ่งนี้อาจไม่มีที่สิ้นสุดดังนั้นความไม่มีที่สิ้นสุดอาจเป็นคำตอบของคุณ


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

0

แม้ว่าโดยทั่วไปจะเป็นไปไม่ได้ที่จะทำตามที่ถูกถาม แต่ก็ยังมีประโยชน์ที่จะนับจำนวนรายการที่ทำซ้ำหลังจากทำซ้ำแล้วซ้ำอีก สำหรับสิ่งนั้นคุณสามารถใช้jaraco.itertools.Counterหรือที่คล้ายกัน นี่คือตัวอย่างการใช้ Python 3 และrwtเพื่อโหลดแพ็คเกจ

$ rwt -q jaraco.itertools -- -q
>>> import jaraco.itertools
>>> items = jaraco.itertools.Counter(range(100))
>>> _ = list(counted)
>>> items.count
100
>>> import random
>>> def gen(n):
...     for i in range(n):
...         if random.randint(0, 1) == 0:
...             yield i
... 
>>> items = jaraco.itertools.Counter(gen(100))
>>> _ = list(counted)
>>> items.count
48


-1

สันนิษฐานว่าคุณต้องการนับจำนวนรายการโดยไม่ต้องวนซ้ำเพื่อไม่ให้ตัววนซ้ำหมดและคุณจะใช้อีกครั้งในภายหลัง เป็นไปได้ด้วยcopyหรือdeepcopy

import copy

def get_iter_len(iterator):
    return sum(1 for _ in copy.copy(iterator))

###############################################

iterator = range(0, 10)
print(get_iter_len(iterator))

if len(tuple(iterator)) > 1:
    print("Finding the length did not exhaust the iterator!")
else:
    print("oh no! it's all gone")

ผลลัพธ์คือ " Finding the length did not exhaust the iterator!"

คุณสามารถเลือก (และไม่มีการควบคุม) คุณสามารถเงาlenฟังก์ชันในตัวได้ดังนี้:

import copy

def len(obj, *, len=len):
    try:
        if hasattr(obj, "__len__"):
            r = len(obj)
        elif hasattr(obj, "__next__"):
            r = sum(1 for _ in copy.copy(obj))
        else:
            r = len(obj)
    finally:
        pass
    return r

1
ช่วงไม่ใช่ตัวทำซ้ำ มีตัวทำซ้ำบางประเภทที่สามารถคัดลอกได้ แต่บางประเภทอาจทำให้รหัสนี้ล้มเหลวด้วย TypeError (เช่นเครื่องกำเนิดไฟฟ้า) และการทำซ้ำผ่านตัวทำซ้ำที่คัดลอกอาจทำให้เกิดผลข้างเคียงสองครั้งหรือทำให้โค้ดแตกโดยพลการซึ่งกล่าวว่า ส่งคืนตัวmapวนซ้ำโดยคาดว่าการเรียกฟังก์ชันผลลัพธ์จะเกิดขึ้นเพียงครั้งเดียว
user2357112 รองรับ Monica
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.