วิธีการดึงองค์ประกอบออกจากชุดโดยไม่ลบออก


427

สมมติว่าต่อไปนี้:

>>> s = set([1, 2, 3])

ฉันจะได้รับค่า (มูลค่าใด ๆ ) sโดยไม่ทำs.pop()อะไรได้อย่างไร ฉันต้องการออกจากรายการในชุดจนกว่าฉันจะแน่ใจว่าฉันสามารถลบมัน - สิ่งที่ฉันสามารถมั่นใจได้หลังจากโทรไม่ตรงกันไปยังโฮสต์อื่น

รวดเร็วและสกปรก:

>>> elem = s.pop()
>>> s.add(elem)

แต่คุณรู้วิธีที่ดีกว่านี้ไหม? นึกคิดในเวลาคงที่


8
ใครรู้บ้างว่าทำไมไพ ธ อนไม่ได้มีการใช้งานฟังก์ชั่นนี้อยู่?
hlin117

กรณีการใช้งานคืออะไร? ชุดไม่มีความสามารถนี้ด้วยเหตุผล คุณควรจะวนซ้ำมันและตั้งค่าการดำเนินการที่เกี่ยวข้องเช่นunionฯลฯ ไม่รับองค์ประกอบจากมัน ตัวอย่างเช่นnext(iter({3,2,1}))จะส่งคืนเสมอ1ดังนั้นหากคุณคิดว่าสิ่งนี้จะส่งคืนองค์ประกอบแบบสุ่ม - จะไม่เกิดขึ้น ดังนั้นบางทีคุณแค่ใช้โครงสร้างข้อมูลที่ไม่ถูกต้อง? กรณีการใช้งานคืออะไร?
user1685095

1
เกี่ยวข้อง: stackoverflow.com/questions/20625579/… (ฉันรู้ว่ามันไม่ใช่คำถามเดียวกัน แต่มีทางเลือกและข้อมูลเชิงลึกที่คุ้มค่า)
John Y

@ hlin117 เพราะชุดเป็นคอลเลกชันเรียงลำดับ เนื่องจากไม่คาดว่าจะมีคำสั่งซื้อจึงไม่มีเหตุผลที่จะเรียกคืนองค์ประกอบที่ตำแหน่งที่กำหนด - คาดว่าจะสุ่ม
Jeyekomon

คำตอบ:


545

สองตัวเลือกที่ไม่ต้องการคัดลอกทั้งชุด:

for e in s:
    break
# e is now an element from s

หรือ...

e = next(iter(s))

แต่โดยทั่วไปชุดไม่สนับสนุนการจัดทำดัชนีหรือการแบ่ง


4
นี่ตอบคำถามของฉัน อนิจจาฉันคิดว่าฉันจะยังคงใช้ป๊อป () เนื่องจากการทำซ้ำดูเหมือนจะเรียงลำดับองค์ประกอบ ฉันจะชอบพวกเขาในการสุ่ม ...
Daren โทมัส

9
ฉันไม่คิดว่า iter () กำลังเรียงลำดับองค์ประกอบ - เมื่อฉันสร้างชุดและป๊อป () จนกระทั่งว่างเปล่าฉันจะเรียงลำดับ (เรียงลำดับตามตัวอย่างของฉัน) และมันก็เหมือนกับ iterator - pop ( ) ไม่สัญญาแบบสุ่มเพียงอย่างเดียวตามที่ "ฉันสัญญาอะไร"
Blair Conrad

2
+1 iter(s).next()ไม่ดี แต่ก็ดี โดยทั่วไปโดยสมบูรณ์เพื่อใช้องค์ประกอบโดยพลการจากวัตถุ iterable ใด ๆ ทางเลือกของคุณถ้าคุณต้องการระวังถ้าคอลเลกชันนั้นว่างเปล่า
u0b34a0f6ae

8
ต่อไป (iter) ก็โอเคและฉันมักจะคิดว่ามันอ่านได้ดีกว่า นอกจากนี้คุณยังสามารถใช้ Sentinel เพื่อจัดการกรณีและปัญหาเมื่อ s ว่างเปล่า เช่นถัดไป (iter, set ())
ja

5
next(iter(your_list or []), None)เพื่อจัดการกับชุดไม่มีและชุดที่ว่างเปล่า
MrE

111

โค้ดที่น้อยที่สุดจะเป็น:

>>> s = set([1, 2, 3])
>>> list(s)[0]
1

เห็นได้ชัดว่านี่จะสร้างรายการใหม่ซึ่งมีสมาชิกแต่ละคนของชุดดังนั้นไม่ดีถ้าชุดของคุณมีขนาดใหญ่มาก


96
next(iter(s))เพียงเกินlist(s)[0]โดยตัวละครทั้งสามและเป็นอย่างอื่นอย่างเห็นได้ชัดที่เหนือกว่าทั้งในเวลาและความซับซ้อนของพื้นที่ ดังนั้นในขณะที่การอ้างถึง "รหัสน้อยที่สุด" เป็นเรื่องจริงเล็กน้อย แต่ก็เป็นความจริงเล็กน้อยว่านี่เป็นวิธีที่เลวร้ายที่สุดที่เป็นไปได้ แม้แต่การลบด้วยตนเองแล้วเพิ่มองค์ประกอบที่ลบไปยังชุดเดิมใหม่จะดีกว่า "สร้างคอนเทนเนอร์ใหม่ทั้งหมดเพียงเพื่อแยกองค์ประกอบแรก" ซึ่งบ้าบิ่น สิ่งที่ฉันเป็นห่วงมากคือ 38 Stackoverflowers สนับสนุนสิ่งนี้จริง ๆ ฉันเพิ่งรู้ว่าฉันจะเห็นสิ่งนี้ในรหัสการผลิต
เซซิลแกงกะหรี่

19
@augurar: เพราะมันทำให้งานเสร็จในลักษณะที่ค่อนข้างง่าย และบางครั้งนั่นคือทั้งหมดที่สำคัญในสคริปต์อย่างรวดเร็ว
tonysdg

4
@Vicrobot ใช่ แต่ทำได้โดยการคัดลอกคอลเลกชันทั้งหมดและเปลี่ยนการดำเนินการ O (1) เป็นการดำเนินการ O (n) นี่เป็นทางออกที่น่ากลัวที่ไม่มีใครควรใช้
สิงหาคม

9
นอกจากนี้หากคุณเพิ่งตั้งเป้าหมาย "รหัสน้อยที่สุด" (ซึ่งโง่เง่า) จากนั้นmin(s)ใช้อักขระน้อยลงในขณะที่แย่และไม่มีประสิทธิภาพเท่านี้
สิงหาคม

5
+1 สำหรับผู้ชนะกอล์ฟรหัสซึ่งฉันมีตัวอย่างที่เป็นประโยชน์สำหรับการเป็น "แย่และไม่มีประสิทธิภาพ": min(s)เร็วกว่าnext(iter(s))ชุดขนาด 1 เล็กน้อยและฉันมาถึงคำตอบนี้โดยเฉพาะการค้นหากรณีพิเศษที่แยกองค์ประกอบจากชุดเท่านั้น ขนาด 1
lehiester

49

ฉันสงสัยว่าฟังก์ชั่นจะทำงานอย่างไรสำหรับชุดที่แตกต่างกันดังนั้นฉันจึงทำเกณฑ์มาตรฐาน:

from random import sample

def ForLoop(s):
    for e in s:
        break
    return e

def IterNext(s):
    return next(iter(s))

def ListIndex(s):
    return list(s)[0]

def PopAdd(s):
    e = s.pop()
    s.add(e)
    return e

def RandomSample(s):
    return sample(s, 1)

def SetUnpacking(s):
    e, *_ = s
    return e

from simple_benchmark import benchmark

b = benchmark([ForLoop, IterNext, ListIndex, PopAdd, RandomSample, SetUnpacking],
              {2**i: set(range(2**i)) for i in range(1, 20)},
              argument_name='set size',
              function_aliases={first: 'First'})

b.plot()

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

พล็อตนี้แสดงให้เห็นอย่างชัดเจนว่าบางวิธี ( RandomSample, SetUnpackingและListIndex) ขึ้นอยู่กับขนาดของชุดและควรหลีกเลี่ยงในกรณีทั่วไป (อย่างน้อยถ้าผลการดำเนินงานอาจจะมีความสำคัญ) ในฐานะที่เป็นแสดงให้เห็นแล้วโดยคำตอบอื่น ๆ ForLoopวิธีที่เร็วที่สุดคือ

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


iteration_utilities(ข้อจำกัดความรับผิดชอบ: ฉันเป็นผู้เขียน) มีฟังก์ชั่นอำนวยความสะดวกสำหรับการใช้งานนี้first:

>>> from iteration_utilities import first
>>> first({1,2,3,4})
1

ฉันรวมมันไว้ในเกณฑ์มาตรฐานข้างต้น มันสามารถแข่งขันกับโซลูชัน "เร็ว" อีกสองตัวได้ แต่ความแตกต่างนั้นไม่เหมือนกัน


43

TL; DR

for first_item in muh_set: breakยังคงเป็นวิธีการที่ดีที่สุดใน Python 3.x สาปแช่งคุณกุย

คุณทำสิ่งนี้

ยินดีต้อนรับสู่การตั้งค่า Python 3.x อีกชุดหนึ่งคาดการณ์จากwr 's ที่ดีหลามตอบสนอง 2.x เฉพาะ ซึ่งแตกต่างจากการตอบสนองเฉพาะ Python 3.xของAChampion ที่มีประโยชน์เท่ากันการกำหนดเวลาด้านล่างนี้ยังมีวิธีแก้ไขปัญหาค่าใช้จ่ายนอกเวลาที่แนะนำข้างต้น - รวมถึง:

  • list(s)[0], จอห์น 's นวนิยายวิธีการแก้ปัญหาตามลำดับ
  • random.sample(s, 1), dF 's ผสมผสานวิธีการแก้ปัญหา RNG ตาม

ตัวอย่างโค้ดสำหรับ Great Joy

เปิดปรับแต่งเวลา:

from timeit import Timer

stats = [
    "for i in range(1000): \n\tfor x in s: \n\t\tbreak",
    "for i in range(1000): next(iter(s))",
    "for i in range(1000): s.add(s.pop())",
    "for i in range(1000): list(s)[0]",
    "for i in range(1000): random.sample(s, 1)",
]

for stat in stats:
    t = Timer(stat, setup="import random\ns=set(range(100))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

หมดเวลาการกำหนดเวลาล้าสมัยอย่างรวดเร็ว

ดูเถิด! สั่งโดยสนิปเพตที่ช้าที่สุด:

$ ./test_get.py
Time for for i in range(1000): 
    for x in s: 
        break:   0.249871
Time for for i in range(1000): next(iter(s)):    0.526266
Time for for i in range(1000): s.add(s.pop()):   0.658832
Time for for i in range(1000): list(s)[0]:   4.117106
Time for for i in range(1000): random.sample(s, 1):  21.851104

ภาพใบหน้าสำหรับทั้งครอบครัว

การทำซ้ำแบบแมนวลยังคงเป็นไปอย่างรวดเร็วเป็นสองเท่าของวิธีแก้ปัญหาที่เร็วที่สุดถัดไป แม้ว่าช่องว่างจะลดลงจาก Bad Old Python 2.x วัน (ซึ่งการวนซ้ำแบบแมนวลอย่างน้อย 4 ครั้ง) มันทำให้PEP 20 มีความกระตือรือร้นในตัวฉันมากที่สุด อย่างน้อยการแปลงชุดเป็นรายการเพียงเพื่อแยกองค์ประกอบแรกของชุดนั้นน่ากลัวอย่างที่คาดไว้ ขอบคุณ Guido ขอให้แสงของเขาดำเนินต่อไปเพื่อนำทางเรา

น่าแปลกที่โซลูชันที่ใช้ RNG นั้นน่ากลัวอย่างยิ่ง การแปลงรายการไม่ดี แต่ใช้เค้กซอสอันยิ่งใหญ่random จริงๆ มากสำหรับRandom Number พระเจ้า

ฉันแค่หวังว่าพวกอสัณฐานพวกเขาจะ PEP set.get_first()วิธีสำหรับเราแล้ว หากคุณกำลังอ่านสิ่งนี้พวกเขา: "ได้โปรดทำอะไรซักอย่าง"


2
ฉันคิดว่าการบ่นว่าnext(iter(s)) ช้ากว่าสองเท่าfor x in s: breakในCPythonนั้นเป็นเรื่องแปลก CPythonฉันหมายความว่าเป็น มันจะช้ากว่า C หรือ Haskell ประมาณ 50-100 เท่า (หรือเกือบทุกครั้งโดยเฉพาะอย่างยิ่งในการทำซ้ำไม่มีการตัดหางและไม่มีการเพิ่มประสิทธิภาพใด ๆ ) การสูญเสียไมโครวินาทีบางครั้งไม่ได้สร้างความแตกต่างอย่างแท้จริง คุณไม่คิดเหรอ และยังมี PyPy
user1685095

39

เพื่อให้ตัวเลขเวลาที่อยู่เบื้องหลังวิธีการที่แตกต่างกันพิจารณารหัสต่อไปนี้ get () เป็นส่วนเติมแต่งที่กำหนดเองของฉันใน python setobject.c เป็นเพียง pop () โดยไม่ต้องลบองค์ประกอบ

from timeit import *

stats = ["for i in xrange(1000): iter(s).next()   ",
         "for i in xrange(1000): \n\tfor x in s: \n\t\tbreak",
         "for i in xrange(1000): s.add(s.pop())   ",
         "for i in xrange(1000): s.get()          "]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100))")
    try:
        print "Time for %s:\t %f"%(stat, t.timeit(number=1000))
    except:
        t.print_exc()

ผลลัพธ์คือ:

$ ./test_get.py
Time for for i in xrange(1000): iter(s).next()   :       0.433080
Time for for i in xrange(1000):
        for x in s:
                break:   0.148695
Time for for i in xrange(1000): s.add(s.pop())   :       0.317418
Time for for i in xrange(1000): s.get()          :       0.146673

ซึ่งหมายความว่าโซลูชันfor / breakเป็นโซลูชันที่เร็วที่สุด (บางครั้งเร็วกว่าโซลูชัน get () ที่กำหนดเอง)


ไม่มีใครมีความคิดว่าทำไม iter (s). ถัดไป () ช้ากว่าความเป็นไปได้อื่น ๆ มากช้ากว่า s.add (s.pop ())? สำหรับฉันแล้วมันรู้สึกว่าการออกแบบ iter () และถัดไป () แย่มากถ้าการกำหนดเวลาเป็นแบบนั้น
peschü

สำหรับหนึ่งบรรทัดนั้นจะสร้างออบเจ็กต์ตัววนซ้ำใหม่ในแต่ละรอบ
Ryan

3
@Ryan: ไม่ใช่วัตถุตัววนซ้ำที่สร้างขึ้นโดยปริยายfor x in sด้วยหรือไม่ "ตัววนซ้ำถูกสร้างขึ้นเพื่อผลลัพธ์ของexpression_list"
musiphil

2
@musiphil นั่นเป็นเรื่องจริง เดิมทีฉันพลาดการ "หยุด" อันใดอันหนึ่งที่ 0.14 ซึ่งเป็นการโต้กลับที่ใช้งานง่าย ฉันต้องการที่จะดำน้ำลึกในนี้เมื่อฉันมีเวลา
Ryan

1
ฉันรู้ว่านี่เก่า แต่เมื่อเพิ่มs.remove()ลงในiterตัวอย่างผสมทั้งสองforและiterไปในทางที่เลวร้าย
AChampion

28

เนื่องจากคุณต้องการองค์ประกอบแบบสุ่มสิ่งนี้จะใช้งานได้:

>>> import random
>>> s = set([1,2,3])
>>> random.sample(s, 1)
[2]

random.sampleเอกสารไม่ได้ดูเหมือนจะพูดถึงประสิทธิภาพการทำงานของ จากการทดสอบเชิงประจักษ์อย่างรวดเร็วด้วยรายการขนาดใหญ่และชุดใหญ่ดูเหมือนว่าจะเป็นเวลาคงที่สำหรับรายการ แต่ไม่ใช่สำหรับชุด นอกจากนี้การทำซ้ำในชุดจะไม่สุ่ม คำสั่งนั้นไม่ได้กำหนด แต่สามารถคาดเดาได้:

>>> list(set(range(10))) == range(10)
True 

หากการสุ่มเป็นสิ่งสำคัญและคุณต้องการองค์ประกอบจำนวนมากในเวลาคงที่ (ชุดใหญ่) ฉันจะใช้random.sampleและแปลงเป็นรายการแรก:

>>> lst = list(s) # once, O(len(s))?
...
>>> e = random.sample(lst, 1)[0] # constant time

14
หากคุณต้องการแค่องค์ประกอบเดียว random.choice ก็สมเหตุสมผลดีกว่า
Gregg Lind

รายการ. ป็อป () จะทำถ้าคุณไม่สนใจองค์ประกอบที่จะรับ
Evgeny

8
@Gregg: คุณไม่สามารถใช้งานได้choice()เพราะ Python จะพยายามจัดทำดัชนีชุดของคุณและใช้งานไม่ได้
Kevin

3
ในขณะที่ฉลาดนี่เป็นทางออกที่ช้าที่สุดที่แนะนำโดยลำดับความสำคัญ ใช่มันเป็นที่ช้า แม้การแปลงชุดเป็นรายการเพียงเพื่อแยกองค์ประกอบแรกของรายการนั้นเร็วขึ้น สำหรับผู้ศรัทธาที่ไม่ใช่ในหมู่พวกเรา ( ... Hi! ) เห็นเหล่านี้กำหนดเวลาที่ยอดเยี่ยม
เซซิลแกงกะหรี่

9

ดูเหมือนว่ากะทัดรัดมากที่สุด (6 สัญญลักษณ์) แม้ว่าจะเป็นวิธีที่ช้ามากในการรับชุดองค์ประกอบ (ทำได้โดยPEP 3132 ):

e,*_=s

ด้วย Python 3.5+ คุณสามารถใช้นิพจน์ 7 สัญลักษณ์นี้ได้ (ด้วยPEP 448 ):

[*s][0]

ตัวเลือกทั้งสองนั้นช้ากว่า 1,000 ครั้งบนเครื่องของฉันมากกว่าวิธี for-loop


1
วิธีการวนรอบ (หรือวิธีการวนซ้ำอย่างแม่นยำ) มีความซับซ้อนของเวลา O (1) ในขณะที่วิธีการเหล่านี้คือ O (N) แม้ว่าพวกเขาจะกระชับ :)
ForeverWintr

6

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

def anyitem(iterable):
    try:
        return iter(iterable).next()
    except StopIteration:
        return None

2
คุณสามารถไปกับถัดไป (iterable (iterable), None) เพื่อประหยัดหมึก :)
1 ''

3

กำลังติดตาม @wr โพสต์ฉันได้รับผลลัพธ์ที่คล้ายกัน (สำหรับ Python3.5)

from timeit import *

stats = ["for i in range(1000): next(iter(s))",
         "for i in range(1000): \n\tfor x in s: \n\t\tbreak",
         "for i in range(1000): s.add(s.pop())"]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100000))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

เอาท์พุท:

Time for for i in range(1000): next(iter(s)):    0.205888
Time for for i in range(1000): 
    for x in s: 
        break:                                   0.083397
Time for for i in range(1000): s.add(s.pop()):   0.226570

อย่างไรก็ตามเมื่อเปลี่ยนสิ่งที่ตั้งไว้ (เช่นการเรียกremove()) สิ่งที่ไม่ดีสำหรับตัวอย่างที่ทำซ้ำได้ ( for, iter):

from timeit import *

stats = ["while s:\n\ta = next(iter(s))\n\ts.remove(a)",
         "while s:\n\tfor x in s: break\n\ts.remove(x)",
         "while s:\n\tx=s.pop()\n\ts.add(x)\n\ts.remove(x)"]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100000))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

ผลลัพธ์ใน:

Time for while s:
    a = next(iter(s))
    s.remove(a):             2.938494
Time for while s:
    for x in s: break
    s.remove(x):             2.728367
Time for while s:
    x=s.pop()
    s.add(x)
    s.remove(x):             0.030272

1

สิ่งที่ฉันมักจะทำสำหรับคอลเล็กชั่นเล็ก ๆ คือการสร้างวิธีการแยกวิเคราะห์ / แปลงเช่นนี้

def convertSetToList(setName):
return list(setName)

จากนั้นฉันสามารถใช้รายการใหม่และเข้าถึงได้ตามหมายเลขดัชนี

userFields = convertSetToList(user)
name = request.json[userFields[0]]

ในรายการคุณจะมีวิธีอื่นทั้งหมดที่คุณอาจต้องใช้ด้วย


ทำไมไม่ใช้เพียงแค่listแทนที่จะสร้างวิธีการแปลง?
Daren Thomas

-1

แล้วไงs.copy().pop()ล่ะ ฉันยังไม่ได้กำหนดเวลา แต่ควรใช้งานได้และง่าย มันทำงานได้ดีที่สุดสำหรับชุดเล็ก ๆ แต่ก็คัดลอกทั้งชุด


-6

อีกทางเลือกหนึ่งคือการใช้พจนานุกรมที่มีค่าที่คุณไม่สนใจ เช่น,


poor_man_set = {}
poor_man_set[1] = None
poor_man_set[2] = None
poor_man_set[3] = None
...

คุณสามารถใช้งานคีย์เป็นชุดยกเว้นว่าเป็นเพียงอาเรย์:


keys = poor_man_set.keys()
print "Some key = %s" % keys[0]

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

แก้ไข: คุณสามารถทำสิ่งนี้เพื่อซ่อนความจริงที่ว่าคุณใช้ dict แทนที่จะเป็นอาร์เรย์หรือชุด:


poor_man_set = {}
poor_man_set[1] = None
poor_man_set[2] = None
poor_man_set[3] = None
poor_man_set = poor_man_set.keys()

3
วิธีนี้ใช้ไม่ได้ตามที่คุณหวัง ใน python 2 keys () เป็นการดำเนินการ O (n) ดังนั้นคุณจึงไม่มีเวลาคงที่อีกต่อไป แต่อย่างน้อยปุ่ม [0] จะคืนค่าที่คุณคาดไว้ ใน python 3 keys () เป็นการปฏิบัติการ O (1) ดังนั้นเลย! อย่างไรก็ตามมันจะไม่ส่งคืนวัตถุรายการอีกต่อไป แต่จะส่งคืนวัตถุที่มีลักษณะเหมือนชุดที่ไม่สามารถทำดัชนีได้ดังนั้นคีย์ [0] จะโยน TypeError stackoverflow.com/questions/39219065/…
sage88
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.