Làm thế nào để lấy một phần tử từ một tập hợp mà không loại bỏ nó?


426

Giả sử như sau:

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

Làm thế nào để tôi có được một giá trị (bất kỳ giá trị) nào smà không làm gì s.pop()? Tôi muốn để lại mục trong bộ cho đến khi tôi chắc chắn mình có thể xóa nó - điều mà tôi chỉ có thể chắc chắn sau một cuộc gọi không đồng bộ đến một máy chủ khác.

Nhanh chóng và hèn hạ:

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

Nhưng bạn có biết một cách tốt hơn? Lý tưởng trong thời gian liên tục.


8
Bất cứ ai cũng biết tại sao python không có chức năng này được thực hiện?
hlin 117

Trường hợp sử dụng là gì? Set không có khả năng này vì một lý do. Bạn phải lặp đi lặp lại qua nó và thực hiện các thao tác liên quan như unionvv không lấy các phần tử từ nó. Ví dụ next(iter({3,2,1}))luôn trả về 1vì vậy nếu bạn nghĩ rằng điều này sẽ trả về phần tử ngẫu nhiên - thì không. Vì vậy, có thể bạn chỉ sử dụng cấu trúc dữ liệu sai? Trường hợp sử dụng là gì?
dùng1685095

1
Liên quan: stackoverflow.com/questions/20625579/ (Tôi biết, đó không phải là câu hỏi tương tự, nhưng có những lựa chọn và hiểu biết đáng giá ở đó.)
John Y

@ hlin117 Vì bộ là một bộ sưu tập không có thứ tự . Vì không có thứ tự nào được mong đợi, nên việc lấy một phần tử tại vị trí đã cho là vô nghĩa - nó được dự kiến ​​là ngẫu nhiên.
Jeyekomon

Câu trả lời:


543

Hai tùy chọn không yêu cầu sao chép toàn bộ:

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

Hoặc là...

e = next(iter(s))

Nhưng nói chung, các bộ không hỗ trợ lập chỉ mục hoặc cắt.


4
Điều này trả lời câu hỏi của tôi. Than ôi, tôi đoán tôi vẫn sẽ sử dụng pop (), vì phép lặp dường như sắp xếp các phần tử. Tôi thích chúng theo thứ tự ngẫu nhiên ...
Daren Thomas

9
Tôi không nghĩ rằng iter () đang sắp xếp các phần tử - khi tôi tạo một tập hợp và pop () cho đến khi nó trống, tôi nhận được thứ tự nhất quán (được sắp xếp, trong ví dụ của tôi) và nó giống như iterator - pop ( ) không hứa hẹn thứ tự ngẫu nhiên, chỉ tùy ý, như trong "Tôi không hứa gì cả".
Blair Conrad

2
+1 iter(s).next()không phải là thô nhưng tuyệt vời. Hoàn toàn chung chung để lấy phần tử tùy ý từ bất kỳ đối tượng lặp lại. Sự lựa chọn của bạn nếu bạn muốn cẩn thận nếu bộ sưu tập trống.
u0b34a0f6ae

8
next (iter (s)) cũng ổn và tôi có xu hướng nghĩ rằng nó đọc tốt hơn. Ngoài ra, bạn có thể sử dụng một sentinel để xử lý trường hợp khi s trống. Ví dụ: next (iter (s), set ()).
ja

5
next(iter(your_list or []), None)để xử lý Không có bộ nào và bộ trống
MrE

109

Mã ít nhất sẽ là:

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

Rõ ràng điều này sẽ tạo ra một danh sách mới chứa mỗi thành viên của tập hợp, vì vậy sẽ không tuyệt vời nếu tập hợp của bạn rất lớn.


94
next(iter(s))chỉ vượt list(s)[0]qua ba nhân vật và là trường hợp đột ngột vượt trội trong cả thời gian và phức tạp không gian. Vì vậy, trong khi tuyên bố về "mã ít nhất" là đúng sự thật, thì cũng đúng một cách tầm thường rằng đây là cách tiếp cận tồi tệ nhất có thể. Ngay cả việc gỡ bỏ thủ công và sau đó thêm lại phần tử đã xóa vào bộ gốc vẫn vượt trội hơn so với "xây dựng một bộ chứa hoàn toàn mới chỉ để trích xuất phần tử đầu tiên", một cách điên rồ. Điều tôi quan tâm hơn là 38 Stackoverflowers thực sự đã nâng cao điều này. Tôi chỉ biết tôi sẽ thấy điều này trong mã sản xuất.
Cecil Curry

19
@augurar: Bởi vì nó hoàn thành công việc một cách tương đối đơn giản. Và đôi khi đó là tất cả những gì quan trọng trong một kịch bản nhanh chóng.
tonysdg

4
@Vicrobot Vâng, nhưng nó làm như vậy bằng cách sao chép toàn bộ bộ sưu tập và biến một hoạt động O (1) thành một hoạt động O (n). Đây là một giải pháp khủng khiếp mà không ai nên sử dụng.
augurar

9
Ngoài ra, nếu bạn chỉ nhắm đến "mã ít nhất" (bị câm), thì sẽ min(s)sử dụng ít ký tự hơn trong khi cũng khủng khiếp và không hiệu quả như thế này.
augurar

5
+1 cho người chiến thắng mã golf, mà tôi có một ví dụ thực tế là "khủng khiếp và không hiệu quả": min(s)nhanh hơn một chút so next(iter(s))với các bộ kích thước 1 và tôi đã đi đến câu trả lời này đặc biệt tìm kiếm trường hợp đặc biệt trích xuất phần tử duy nhất từ ​​các bộ có kích thước 1.
lehiester

48

Tôi tự hỏi làm thế nào các chức năng sẽ thực hiện cho các bộ khác nhau, vì vậy tôi đã làm một điểm chuẩn:

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

nhập mô tả hình ảnh ở đây

Cốt truyện này rõ ràng cho thấy rằng một số phương pháp tiếp cận ( RandomSample, SetUnpackingListIndex) phụ thuộc vào kích thước của bộ và nên tránh trong trường hợp chung (ít nhất là nếu hiệu suất có thể là quan trọng). Như đã được hiển thị bởi các câu trả lời khác cách nhanh nhất là ForLoop.

Tuy nhiên, miễn là một trong những cách tiếp cận thời gian không đổi được sử dụng, sự khác biệt hiệu suất sẽ không đáng kể.


iteration_utilities(Tuyên bố miễn trừ trách nhiệm: Tôi là tác giả) có chức năng tiện lợi cho trường hợp sử dụng này first::

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

Tôi cũng bao gồm nó trong điểm chuẩn ở trên. Nó có thể cạnh tranh với hai giải pháp "nhanh" khác nhưng sự khác biệt không phải là nhiều.


43

tl; dr

for first_item in muh_set: breakvẫn là cách tiếp cận tối ưu trong Python 3.x. Nguyền rủa bạn, Guido.

bạn làm điều này

Chào mừng bạn đến với một bộ thời gian Python 3.x khác, ngoại suy từ wr. là tuyệt vời Python 2.x phản ứng cụ thể . Không giống như phản hồi dành riêng cho Python 3.x của AChampion , các thời gian bên dưới cũng có các giải pháp ngoại lệ được đề xuất ở trên - bao gồm:

Đoạn mã cho niềm vui lớn

Bật, điều chỉnh, thời gian nó:

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

Nhanh chóng lỗi thời vượt thời gian

Hãy chứng kiến! Được sắp xếp theo đoạn nhanh nhất đến chậm nhất:

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

Khẩu trang cho cả gia đình

Không có gì đáng ngạc nhiên, việc lặp lại thủ công vẫn nhanh nhất ít nhất gấp đôi so với giải pháp nhanh nhất tiếp theo. Mặc dù khoảng cách đã giảm từ Bad Old Python 2.x ngày (trong đó việc lặp lại thủ công nhanh nhất ít nhất bốn lần), nhưng điều đó làm tôi thất vọng với PEP 20 zealot trong tôi rằng giải pháp dài dòng nhất là tốt nhất. Ít nhất là chuyển đổi một tập hợp thành một danh sách chỉ để trích xuất phần tử đầu tiên của tập hợp là khủng khiếp như mong đợi. Cảm ơn Guido, có thể ánh sáng của anh ấy tiếp tục hướng dẫn chúng tôi.

Đáng ngạc nhiên, giải pháp dựa trên RNG là hoàn toàn khủng khiếp. Chuyển đổi danh sách là xấu, nhưng random thực sự mất bánh nước sốt khủng khiếp. Quá nhiều cho Thiên Chúa số ngẫu nhiên .

Tôi chỉ muốn những người vô định hình họ sẽ đưa ra một set.get_first()phương pháp cho chúng tôi. Nếu bạn đang đọc cái này, họ: "Làm ơn. Làm gì đi."


2
Tôi nghĩ rằng phàn nàn rằng next(iter(s)) chậm hơn hai lần so với for x in s: breaktrong CPythonlà lạ. Ý tôi là đó là CPython. Nó sẽ chậm hơn khoảng 50 - 100 lần (hoặc một cái gì đó tương tự) so với C hoặc Haskell làm điều tương tự (trong phần lớn thời gian, đặc biệt là trong phép lặp, không loại bỏ cuộc gọi đuôi và không tối ưu hóa bất cứ điều gì.). Mất một vài phần triệu giây không tạo ra sự khác biệt thực sự. Bạn không nghĩ sao? Và còn có PyPy
user1685095

39

Để cung cấp một số số liệu thời gian đằng sau các phương pháp khác nhau, hãy xem xét mã sau đây. Get () là phần bổ sung tùy chỉnh của tôi vào setobject.c của Python, chỉ là một pop () mà không xóa phần tử.

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

Đầu ra là:

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

Điều này có nghĩa là giải pháp for / break là nhanh nhất (đôi khi nhanh hơn giải pháp get () tùy chỉnh).


Có ai có ý tưởng tại sao iter (s) .next () lại chậm hơn nhiều so với các khả năng khác, thậm chí chậm hơn s.add (s.pop ()) không? Đối với tôi cảm giác như thiết kế rất tệ của iter () và next () nếu thời gian trông như thế.
peschü

Vâng, đối với một dòng đó tạo ra một đối tượng lặp mới mỗi lần lặp.
Ryan

3
@Ryan: Không phải là một đối tượng lặp cũng được tạo hoàn toàn for x in ssao? "Một trình vòng lặp được tạo cho kết quả của expression_list."
musiphil

2
@musiphil Điều đó đúng; ban đầu tôi đã bỏ lỡ "break" ở mức 0,14, điều đó thực sự phản trực giác. Tôi muốn đi sâu vào vấn đề này khi có thời gian.
Ryan

1
Tôi biết điều này là cũ, nhưng khi thêm s.remove()vào các iterví dụ cả hai foritertrở nên tồi tệ.
AChampion

28

Vì bạn muốn một yếu tố ngẫu nhiên, điều này cũng sẽ hoạt động:

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

Các tài liệu dường như không đề cập đến hiệu suất của random.sample. Từ một thử nghiệm thực nghiệm nhanh chóng với một danh sách khổng lồ và một tập hợp lớn, dường như đó là thời gian không đổi cho một danh sách nhưng không phải cho tập hợp. Ngoài ra, việc lặp lại một bộ không phải là ngẫu nhiên; thứ tự không xác định nhưng có thể dự đoán được:

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

Nếu tính ngẫu nhiên là quan trọng và bạn cần một loạt các yếu tố trong thời gian không đổi (bộ lớn), trước tiên tôi sẽ sử dụng random.samplevà chuyển đổi thành danh sách:

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

14
Nếu bạn chỉ muốn một yếu tố, Random.choice hợp lý hơn.
Gregg Lind

list (s) .pop () sẽ làm nếu bạn không quan tâm đến yếu tố nào.
Evgeny

8
@Gregg: Bạn không thể sử dụng choice(), vì Python sẽ cố gắng lập chỉ mục cho tập hợp của bạn và điều đó không hoạt động.
Kevin

3
Mặc dù thông minh, đây thực sự là giải pháp chậm nhất được đề xuất bởi một mức độ lớn. Vâng, nó chậm. Ngay cả việc chuyển đổi tập hợp thành một danh sách chỉ để trích xuất phần tử đầu tiên của danh sách đó là nhanh hơn. Đối với những người không tin trong số chúng ta ( ... hi! ), Hãy xem những khoảng thời gian tuyệt vời này .
Cecil Curry

9

Dường như nhỏ gọn nhất (6 ký hiệu) mặc dù cách rất chậm để có được một phần tử được thiết lập (được thực hiện bởi PEP 3132 ):

e,*_=s

Với Python 3.5+, bạn cũng có thể sử dụng biểu thức 7 ký hiệu này (nhờ PEP 448 ):

[*s][0]

Cả hai tùy chọn đều chậm hơn khoảng 1000 lần trên máy của tôi so với phương pháp vòng lặp.


1
Phương thức vòng lặp for (hay chính xác hơn là phương thức lặp) có độ phức tạp thời gian O (1), trong khi các phương thức này là O (N). Họ là súc tích mặc dù. :)
ForeverWintr

6

Tôi sử dụng một chức năng tiện ích tôi đã viết. Tên của nó có phần gây hiểu nhầm bởi vì nó ngụ ý nó có thể là một vật phẩm ngẫu nhiên hoặc một cái gì đó tương tự.

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

2
Bạn cũng có thể đi với next (iter (iterable), none) để tiết kiệm mực :)
1 ''

3

Theo dõi @wr. bài đăng, tôi nhận được kết quả tương tự (đối với 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()

Đầu ra:

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

Tuy nhiên, khi thay đổi tập hợp cơ bản (ví dụ như gọi đến remove()), mọi thứ trở nên tồi tệ đối với các ví dụ lặp lại ( 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()

Kết quả trong:

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

Những gì tôi thường làm cho các bộ sưu tập nhỏ là tạo ra phương thức trình phân tích cú pháp / trình chuyển đổi như thế này

def convertSetToList(setName):
return list(setName)

Sau đó tôi có thể sử dụng danh sách mới và truy cập theo số chỉ mục

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

Là một danh sách, bạn sẽ có tất cả các phương thức khác mà bạn có thể cần phải làm việc với


Tại sao không chỉ sử dụng listthay vì tạo một phương thức chuyển đổi?
Daren Thomas

-1

Thế còn s.copy().pop()? Tôi đã không hẹn giờ, nhưng nó sẽ hoạt động và nó đơn giản. Tuy nhiên, nó hoạt động tốt nhất cho các bộ nhỏ, vì nó sao chép toàn bộ.


-6

Một lựa chọn khác là sử dụng từ điển với các giá trị bạn không quan tâm. Ví dụ,


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

Bạn có thể coi các khóa là một bộ trừ khi chúng chỉ là một mảng:


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

Một tác dụng phụ của lựa chọn này là mã của bạn sẽ tương thích ngược với các setphiên bản cũ hơn của Python. Nó có thể không phải là câu trả lời tốt nhất nhưng đó là một lựa chọn khác.

Chỉnh sửa: Bạn thậm chí có thể làm một cái gì đó như thế này để che giấu sự thật rằng bạn đã sử dụng một lệnh thay vì một mảng hoặc bộ:


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
Điều này không hoạt động theo cách bạn hy vọng nó sẽ. Trong python 2 phím () là thao tác O (n), do đó bạn không còn thời gian không đổi, nhưng ít nhất các khóa [0] sẽ trả về giá trị bạn mong đợi. Trong python 3 phím () là một phép toán O (1), vì vậy yay! Tuy nhiên, nó không còn trả về một đối tượng danh sách, nó trả về một đối tượng giống như tập hợp không thể được lập chỉ mục, vì vậy các khóa [0] sẽ ném TypeError. stackoverflow.com/questions/39219065/ từ
sage88
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.