Як отримати елемент із набору, не виймаючи його?


427

Припустимо наступне:

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

Як я можу отримати значення (будь-яке значення), sне виконуючи це s.pop()? Я хочу залишити елемент у наборі до тих пір, поки не буду впевнений, що зможу його видалити - у чому я можу бути впевнений лише після асинхронного дзвінка іншому хосту.

Швидкий і брудний:

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

Але чи знаєте ви кращого способу? Ідеально в постійний час.


8
Хтось знає, чому python ще не реалізував цю функцію?
hlin117

Який випадок використання? Набір не має цієї можливості з причини. Ви повинні переглядати його і робити такі операції, як unionі т.д., не беручи з нього елементів. Наприклад, next(iter({3,2,1}))завжди повертається, 1тому якщо ви думали, що це поверне випадковий елемент - це не буде. То, можливо, ви просто використовуєте неправильну структуру даних? Який випадок використання?
користувач1685095

1
Пов’язано: stackoverflow.com/questions/20625579/… (я знаю, це не одне й те саме питання, але є вагомі альтернативи та розуміння.)
Іоанн Y

@ hlin117 Оскільки набір - це не упорядкована колекція . Оскільки порядку не очікується, витягувати елемент у заданій позиції немає сенсу - він, як очікується, буде випадковим.
Jeyekomon

Відповіді:


545

Два варіанти, які не потребують копіювання всього набору:

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

Або ...

e = next(iter(s))

Але в цілому набори не підтримують індексацію або нарізку.


4
Це відповідає на моє запитання. На жаль, я думаю, я все одно буду використовувати pop (), оскільки ітерація, здається, сортує елементи. Я б віддав перевагу їх у випадковому порядку ...
Дарен Томас,

9
Я не думаю, що iter () сортує елементи - коли я створюю набір і pop (), поки він не порожній, я отримую послідовне (відсортоване, у моєму прикладі) впорядкування, і це те саме, що ітератор - pop ( ) не обіцяє випадкового порядку, просто довільного, як у "Я нічого не обіцяю".
Блер Конрад

2
+1 iter(s).next()не є грубим, але чудовим. Цілком загальне, щоб взяти довільний елемент з будь-якого ітерабельного об'єкта. Ваш вибір, якщо ви хочете бути обережними, якщо колекція порожня.
u0b34a0f6ae

8
next (iter (s)) також добре, і я схильний вважати, що він читає краще. Крім того, ви можете використовувати дозорну для обробки справи, коли s порожній. Напр. Наступний (iter (s), set ()).
JA

5
next(iter(your_list or []), None)обробляти
Без

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

тл; д-р

for first_item in muh_set: breakзалишається оптимальним підходом у Python 3.x. Прокляни тебе, Гвідо.

ти це зробиш

Ласкаво просимо до ще одного набору синхронізацій Python 3.x, екстрапольованого з wr. відмінна відповідь Python 2.x-специфічна . На відміну від не менш корисної для конкретного відповіді Python 3.x для AChampion , наведені нижче терміни також пропонують рішення, що пропонуються вище, включаючи:

Кодові фрагменти для великої радості

Увімкніть, налаштуйте, час:

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 за двадцять днів (коли ручна ітерація була принаймні у чотири рази швидшою), це розчаровує в мене ревника PEP 20 тим, що найголовніше рішення є найкращим. Принаймні перетворення набору в список лише для вилучення першого елемента набору є таким же жахливим, як і очікувалося. Дякую Гуйдо, нехай його світло продовжує нас керувати.

Дивно, але рішення, що базується на СПГ, абсолютно жахливе. Перетворення списків погано, але random насправді приймає жахливий торт-соус. Стільки за Бога випадкового числа .

Я просто бажаю аморфним. Вони б вже підготували set.get_first()метод для нас. Якщо ви читаєте це, вони: "Будь ласка. Робіть щось".


2
Я думаю, скаржитися на те, що next(iter(s)) це вдвічі повільніше, ніж for x in s: breakу, CPythonце щось дивно. Я маю на увазі, що це так CPython. Це буде приблизно в 50-100 разів (або щось подібне) повільніше, ніж C або Haskell, роблячи те ж саме (більшу частину часу, особливо це стосується ітерації, не усунення хвостових викликів і жодних оптимізацій.). Втрата декількох мікросекунд не має реального значення. Ти не думаєш? А ще є PyPy
користувач1685095

39

Щоб забезпечити деякі показники часу, що стоять за різними підходами, врахуйте наступний код. Get () - моє спеціальне доповнення до setobject.c Python, будучи просто 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) .next () набагато повільніше, ніж інші можливості, навіть повільніше, ніж s.add (s.pop ())? Мені здається, що дуже поганий дизайн iter () та next (), якщо таймінги виглядають так.
peschü

Добре для одного рядка створюється новий ітераційний об'єкт кожної ітерації.
Райан

3
@Ryan: Чи не також об’єкт ітератора створений неявно for x in s? "Ітератор створений для результату expression_list."
musiphil

2
@musiphil Це правда; спочатку я пропустив "перерву", будучи 0,14, що насправді контрінтуїтивно. Хочеться глибоко зануритися в це, коли встигну.
Райан

1
Я знаю, що це старе, але коли ви додаєте s.remove()в суміш iterприклади і те, forі iterкатастрофічно погано.
Чемпіон AC

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 є більш розумним.
Грегг Лінд

list (s) .pop () зробить, якщо вам все одно, який елемент брати.
Євгеній

8
@Gregg: Ви не можете використовувати choice(), тому що Python спробує проіндексувати ваш набір, а це не працює.
Кевін

3
Хоча розумна, це насправді найповільніше рішення, яке все-таки пропонується на порядок. Так, це , що повільно. Навіть перетворення набору в список лише для вилучення першого елемента цього списку відбувається швидше. Для невіруючих серед нас ( ... привіт! ) Дивіться ці казкові таймінги .
Сесіль Карі

9

Здавалося б , що найбільш компактні (6 символів) , хоча дуже повільний спосіб отримати безліч елементів ( що стало можливим завдяки PEP 3132 ):

e,*_=s

За допомогою Python 3.5+ ви також можете використовувати цей вираз із 7 символів (завдяки PEP 448 ):

[*s][0]

Обидва варіанти на моїй машині приблизно в 1000 разів повільніше, ніж метод for-loop.


1
Метод for циклу (або точніше метод ітератора) має часову складність O (1), тоді як ці методи O (N). Вони хоч і лаконічні . :)
ForeverWintr

6

Я використовую функцію утиліти, яку я написав. Його назва дещо вводить в оману, оскільки з цього виду випливає, що це може бути випадковий предмет або щось подібне.

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

2
Ви також можете піти з наступним (iter (ітерабельний), 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замість створення методу перетворення?
Дарен Томас

-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]

Побічним ефектом цього вибору є те, що ваш код буде сумісним із старими попередніми setверсіями Python. Це, можливо, не найкраща відповідь, але це інший варіант.

Редагувати: Ви навіть можете зробити щось подібне, щоб приховати факт, що ви використовували дикт замість масиву чи набору:


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 ключі () - це операція O (n), тому ви більше не є постійним часом, але принаймні клавіші [0] повернуть очікуване значення. У python 3 клавіші () - це операція O (1), так що так! Однак він більше не повертає об'єкт списку, він повертає подібний до набору об’єкт, який не можна індексувати, тому клавіші [0] будуть кидати TypeError. stackoverflow.com/questions/39219065 / ...
sage88
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.