Bagaimana cara mengambil elemen dari suatu set tanpa menghapusnya?

427

Misalkan yang berikut:

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

Bagaimana cara mendapatkan nilai (nilai apa pun) stanpa melakukannya s.pop()? Saya ingin meninggalkan item di set sampai saya yakin saya bisa menghapusnya - sesuatu yang saya hanya bisa yakin setelah panggilan asinkron ke host lain.

Cepat dan kotor:

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

Tetapi apakah Anda tahu cara yang lebih baik? Idealnya dalam waktu yang konstan.

Daren Thomas
sumber
8
Adakah yang tahu mengapa python belum mengimplementasikan fungsi ini?
hlin117
Apa gunanya? Set tidak memiliki kemampuan ini karena suatu alasan. Anda seharusnya mengulanginya dan membuat set operasi terkait seperti uniondll tidak mengambil elemen darinya. Misalnya next(iter({3,2,1}))selalu kembali 1jadi jika Anda berpikir bahwa ini akan mengembalikan elemen acak - tidak akan. Jadi mungkin Anda hanya menggunakan struktur data yang salah? Apa gunanya?
user1685095
1
Terkait: stackoverflow.com/questions/20625579/… (Saya tahu, ini bukan pertanyaan yang sama, tetapi ada alternatif dan wawasan berharga di sana.)
John Y
@ hlin117 Karena set adalah koleksi yang tidak terurut . Karena tidak ada urutan yang diharapkan, maka tidak masuk akal untuk mengambil elemen pada posisi tertentu - diharapkan acak.
Jeyekomon

Jawaban:

545

Dua opsi yang tidak perlu menyalin seluruh rangkaian:

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

Atau...

e = next(iter(s))

Tetapi secara umum, set tidak mendukung pengindeksan atau pemotongan.

Blair Conrad
sumber
4
Ini menjawab pertanyaan saya. Sayangnya, saya kira saya masih akan menggunakan pop (), karena iterasi tampaknya mengurutkan elemen. Saya lebih suka mereka secara acak ...
Daren Thomas
9
Saya tidak berpikir bahwa iter () sedang mengurutkan elemen - ketika saya membuat set dan pop () sampai kosong, saya mendapatkan urutan yang konsisten (diurutkan, dalam contoh saya), dan itu sama dengan iterator - pop ( ) tidak menjanjikan pesanan acak, hanya sewenang - wenang, seperti dalam "Saya tidak menjanjikan apa - apa".
Blair Conrad
2
+1 iter(s).next()tidak kasar tapi bagus. Sepenuhnya umum untuk mengambil elemen sewenang-wenang dari objek yang dapat diubah. Pilihan Anda jika Anda ingin berhati-hati jika koleksinya kosong.
u0b34a0f6ae
8
next (iter) juga OK dan saya cenderung berpikir itu lebih baik. Anda juga dapat menggunakan sentinel untuk menangani kasing saat s kosong. Misalnya selanjutnya (iter (s), set ()).
ja
5
next(iter(your_list or []), None)untuk menangani Tidak ada set dan set kosong
MrE
111

Kode paling tidak adalah:

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

Jelas ini akan membuat daftar baru yang berisi setiap anggota set, jadi tidak bagus jika set Anda sangat besar.

John
sumber
97
next(iter(s))hanya melebihi list(s)[0]oleh tiga karakter dan sebaliknya secara dramatis unggul dalam waktu dan kompleksitas ruang. Jadi, sementara klaim "kode paling sedikit" sepele benar, itu juga sepele benar bahwa ini adalah pendekatan terburuk yang mungkin. Bahkan menghapus secara manual dan kemudian menambahkan kembali elemen yang dihapus ke set asli lebih unggul daripada "membangun sebuah wadah baru hanya untuk mengekstrak elemen pertama," yang jelas-jelas gila. Yang lebih mengkhawatirkan saya adalah bahwa 38 Stackoverflower benar-benar menaikkan peringkat ini. Saya hanya tahu saya akan melihat ini dalam kode produksi.
Cecil Curry
19
@ augurar: Karena menyelesaikan pekerjaan dengan cara yang relatif sederhana. Dan terkadang hanya itu yang penting dalam skrip cepat.
tonysdg
4
@Vicrobot Ya tapi itu dilakukan dengan menyalin seluruh koleksi dan mengubah operasi O (1) menjadi operasi O (n). Ini adalah solusi mengerikan yang tidak seorang pun boleh menggunakannya.
augurar
9
Juga jika Anda hanya bertujuan untuk "kode paling" (yang bodoh), maka min(s)gunakan karakter lebih sedikit sambil menjadi mengerikan dan tidak efisien seperti ini.
augurar
5
+1 untuk pemenang golf kode, yang saya punya contoh tandingan praktis untuk menjadi "mengerikan dan tidak efisien": min(s)sedikit lebih cepat daripada next(iter(s))untuk set ukuran 1, dan saya sampai pada jawaban ini secara khusus mencari kasus khusus yang mengekstraksi satu-satunya elemen dari set ukuran 1.
lehiester
51

Saya bertanya-tanya bagaimana fungsi akan tampil untuk set yang berbeda, jadi saya melakukan benchmark:

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()

masukkan deskripsi gambar di sini

Plot ini jelas menunjukkan bahwa beberapa pendekatan ( RandomSample, SetUnpackingdan ListIndex) bergantung pada ukuran set dan harus dihindari dalam kasus umum (setidaknya jika kinerja mungkin penting). Seperti yang sudah ditunjukkan oleh jawaban lain, cara tercepat adalah ForLoop.

Namun selama salah satu pendekatan waktu konstan digunakan, perbedaan kinerja akan diabaikan.


iteration_utilities(Penafian: Saya penulis) berisi fungsi kenyamanan untuk kasus penggunaan ini first::

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

Saya juga memasukkannya dalam benchmark di atas. Ini dapat bersaing dengan dua solusi "cepat" lainnya tetapi perbedaannya tidak banyak.

MSeifert
sumber
43

tl; dr

for first_item in muh_set: breaktetap menjadi pendekatan optimal dalam Python 3.x. Terkutuklah kamu, Guido.

kamu melakukan ini

Selamat datang di rangkaian Python 3.x lainnya, diekstrapolasi dari wr. Ini sangat baik Python respon 2.x-spesifik . Tidak seperti AChampion yang juga sangat membantu Python 3.x respon spesifik , timing di bawah ini juga time outlier solution yang disarankan di atas - termasuk:

Cuplikan Kode untuk Kegembiraan Besar

Aktifkan, dengar, atur waktu:

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()

Pengaturan waktu abadi cepat

Melihat! Dipesan oleh snippet tercepat hingga paling lambat:

$ ./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

Faceplants untuk Seluruh Keluarga

Tidak mengherankan, iterasi manual tetap setidaknya dua kali lebih cepat dari solusi tercepat berikutnya. Meskipun kesenjangan telah berkurang dari Bad Old Python 2.x hari (di mana iterasi manual setidaknya empat kali lebih cepat), mengecewakan PEP 20 fanatik pada saya bahwa solusi yang paling bertele-tele adalah yang terbaik. Setidaknya mengubah set ke daftar hanya untuk mengekstrak elemen pertama set sama mengerikan seperti yang diharapkan. Terima kasih Guido, semoga cahayanya terus membimbing kita.

Anehnya, solusi berbasis RNG benar-benar mengerikan. Konversi daftar buruk, tetapi random benar - benar memakan kue saus yang enak. Begitu banyak untuk Dewa Angka Acak .

Saya hanya berharap yang amorf. Mereka akan menggunakan set.get_first()metode untuk kita. Jika Anda membaca ini, Mereka: "Tolong. Lakukan sesuatu."

Cecil Curry
sumber
2
Saya pikir mengeluh bahwa itu next(iter(s)) dua kali lebih lambat daripada for x in s: breakdi CPythonagak aneh. Maksud saya itu CPython. Ini akan menjadi sekitar 50-100 kali (atau sesuatu seperti itu) lebih lambat daripada C atau Haskell melakukan hal yang sama (untuk sebagian besar waktu, terutama dalam iterasi, tidak ada penghapusan panggilan ekor dan tidak ada optimasi sama sekali.). Kehilangan beberapa mikrodetik tidak membuat perbedaan nyata. Bukankah begitu? Dan ada juga PyPy
user1685095
39

Untuk memberikan beberapa angka waktu di balik pendekatan yang berbeda, pertimbangkan kode berikut. Get () adalah tambahan kustom saya untuk setobject.c Python, menjadi hanya pop () tanpa menghapus elemen.

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()

Outputnya adalah:

$ ./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

Ini berarti bahwa solusi for / break adalah yang tercepat (terkadang lebih cepat daripada solusi custom get ()).

wr.
sumber
Adakah yang tahu mengapa iter (s) .Next () jauh lebih lambat dari kemungkinan lain, bahkan lebih lambat dari s.add (s.pop ())? Bagi saya rasanya desain iter () dan next () yang sangat buruk jika timingnya seperti itu.
peschü
Nah untuk satu baris itu buat objek iter baru setiap iterasi.
Ryan
3
@Ryan: Bukankah objek iterator dibuat secara implisit for x in sjuga? "Sebuah iterator dibuat untuk hasil expression_list."
musiphil
2
@musiphil Itu benar; awalnya saya melewatkan "break" yang berada di 0,14, itu benar-benar kontra-intuitif. Saya ingin melakukan penyelaman mendalam ketika saya punya waktu.
Ryan
1
Saya tahu ini sudah tua, tetapi ketika menambahkan s.remove()ke dalam campuran itercontoh keduanya fordan itermenjadi buruk.
AChampion
28

Karena Anda ingin elemen acak, ini juga akan berfungsi:

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

Dokumentasi tampaknya tidak menyebutkan kinerja random.sample. Dari tes empiris yang sangat cepat dengan daftar besar dan satu set besar, tampaknya menjadi waktu yang konstan untuk daftar tetapi tidak untuk set. Juga, iterasi pada set tidak acak; pesanan tidak ditentukan tetapi dapat diprediksi:

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

Jika keacakan penting dan Anda membutuhkan banyak elemen dalam waktu yang konstan (kumpulan besar), saya akan menggunakan random.sampledan mengonversi ke daftar terlebih dahulu:

>>> lst = list(s) # once, O(len(s))?
...
>>> e = random.sample(lst, 1)[0] # constant time
dF.
sumber
14
Jika Anda hanya menginginkan satu elemen, random.choice lebih masuk akal.
Gregg Lind
daftar .pop () akan dilakukan jika Anda tidak peduli elemen mana yang harus diambil.
Evgeny
8
@Gregg: Anda tidak dapat menggunakan choice(), karena Python akan mencoba mengindeks set Anda dan itu tidak berhasil.
Kevin
3
Meski pintar, ini sebenarnya solusi paling lambat yang disarankan oleh urutan besarnya. Ya, itu yang lambat. Bahkan mengubah set menjadi daftar hanya untuk mengekstrak elemen pertama dari daftar itu lebih cepat. Untuk yang belum percaya di antara kita ( ... hai! ), Lihat timing yang luar biasa ini .
Cecil Curry
9

Tampaknya yang paling ringkas (6 simbol) meskipun cara yang sangat lambat untuk mendapatkan elemen set (dimungkinkan oleh PEP 3132 ):

e,*_=s

Dengan Python 3.5+ Anda juga dapat menggunakan ekspresi 7-simbol ini (terima kasih kepada PEP 448 ):

[*s][0]

Kedua opsi kira-kira 1000 kali lebih lambat pada mesin saya daripada metode for-loop.

skovorodkin
sumber
1
Metode for loop (atau lebih tepatnya metode iterator) memiliki kompleksitas waktu O (1), sedangkan metode ini adalah O (N). Mereka ringkas . :)
ForeverWintr
6

Saya menggunakan fungsi utilitas yang saya tulis. Namanya agak menyesatkan karena agak menyiratkan itu mungkin item acak atau sesuatu seperti itu.

def anyitem(iterable):
    try:
        return iter(iterable).next()
    except StopIteration:
        return None
Nick
sumber
2
Anda juga dapat menggunakan berikutnya (iter (iterable), None) untuk menghemat tinta :)
1 ''
3

Mengikuti @wr. posting, saya mendapatkan hasil yang serupa (untuk 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()

Keluaran:

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

Namun, ketika mengubah himpunan yang mendasarinya (mis. Panggilan ke remove()) hal-hal buruk terjadi untuk contoh yang dapat diubah ( 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()

Hasil dalam:

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
Acampion
sumber
1

Apa yang biasanya saya lakukan untuk koleksi kecil adalah membuat semacam metode parser / converter seperti ini

def convertSetToList(setName):
return list(setName)

Kemudian saya dapat menggunakan daftar baru dan akses dengan nomor indeks

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

Sebagai daftar, Anda akan memiliki semua metode lain yang mungkin perlu Anda kerjakan

Josué Carvajal
sumber
mengapa tidak menggunakan saja listalih-alih membuat metode konverter?
Daren Thomas
-1

Bagaimana dengan s.copy().pop()? Saya belum menghitung waktunya, tetapi harus berhasil dan sederhana. Ini bekerja paling baik untuk set kecil, karena menyalin seluruh set.

Solomon Ucko
sumber
-6

Pilihan lain adalah menggunakan kamus dengan nilai yang tidak Anda pedulikan. Misalnya,


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

Anda bisa memperlakukan kunci sebagai set kecuali bahwa itu hanya array:


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

Efek samping dari pilihan ini adalah bahwa kode Anda akan kompatibel dengan setPython versi lama. Itu mungkin bukan jawaban terbaik tapi itu pilihan lain.

Sunting: Anda bahkan dapat melakukan sesuatu seperti ini untuk menyembunyikan fakta bahwa Anda menggunakan dict alih-alih array atau set:


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()
Pat Notz
sumber
3
Ini tidak berfungsi seperti yang Anda harapkan. Dalam python 2 kunci () adalah operasi O (n), jadi Anda tidak lagi memiliki waktu yang konstan, tetapi setidaknya kunci [0] akan mengembalikan nilai yang Anda harapkan. Dalam python 3 kunci () adalah operasi O (1), jadi yay! Namun, itu tidak lagi mengembalikan objek daftar, itu mengembalikan objek set-seperti yang tidak dapat diindeks, jadi tombol [0] akan membuang TypeError. stackoverflow.com/questions/39219065/…
sage88