Як я можу перевірити, що список має одне і тільки одне достовірне значення?


84

У python у мене є список, який повинен мати одне і тільки одне істинне значення (тобто, bool(value) is True). Чи є розумний спосіб перевірити це? Зараз я просто перебираю список і перевіряю вручну:

def only1(l)
    true_found = False
    for v in l:
        if v and not true_found:
            true_found=True
        elif v and true_found:
             return False #"Too Many Trues"
    return true_found

Це здається неелегантним і не дуже пітонічним. Чи є розумніший спосіб це зробити?


2
Я думаю, що ваше рішення цілком чудове, і воно пітонічне!
wim

1
Common Lisp: (= 1 (count-if #'identity list)).
Каз

7
sum(lst) == 1
Pål GD

Щоб було зрозуміло: чи хочете ви перевірити, чи є лише одне Trueабо лише одне істинне значення?
Marcin

Відповіді:


44

Найбільш багатослівне рішення - не завжди найелегантніше рішення. Тому я додаю лише незначну модифікацію (з метою збереження надлишкових булевих оцінок):

def only1(l):
    true_found = False
    for v in l:
        if v:
            # a True was found!
            if true_found:
                # found too many True's
                return False 
            else:
                # found the first True
                true_found = True
    # found zero or one True value
    return true_found

Ось кілька термінів для порівняння:

# file: test.py
from itertools import ifilter, islice

def OP(l):
    true_found = False
    for v in l:
        if v and not true_found:
            true_found=True
        elif v and true_found:
             return False #"Too Many Trues"
    return true_found

def DavidRobinson(l):
    return l.count(True) == 1

def FJ(l):
    return len(list(islice(ifilter(None, l), 2))) == 1

def JonClements(iterable):
    i = iter(iterable)
    return any(i) and not any(i)

def moooeeeep(l):
    true_found = False
    for v in l:
        if v:
            if true_found:
                # found too many True's
                return False 
            else:
                # found the first True
                true_found = True
    # found zero or one True value
    return true_found

Мій результат:

$ python -mtimeit -s 'import test; l=[True]*100000' 'test.OP(l)' 
1000000 loops, best of 3: 0.523 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.DavidRobinson(l)' 
1000 loops, best of 3: 516 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.FJ(l)' 
100000 loops, best of 3: 2.31 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.JonClements(l)' 
1000000 loops, best of 3: 0.446 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.moooeeeep(l)' 
1000000 loops, best of 3: 0.449 usec per loop

Як можна бачити, рішення OP є значно кращим, ніж більшість інших рішень, розміщених тут. Як і слід було очікувати, найкращими є ті, що мають поведінку при короткому замиканні, особливо це рішення, розміщене Джоном Клементсом. Принаймні для випадку двох ранніх Trueзначень у довгому списку.

Тут те саме для жодного Trueзначення:

$ python -mtimeit -s 'import test; l=[False]*100000' 'test.OP(l)' 
100 loops, best of 3: 4.26 msec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.DavidRobinson(l)' 
100 loops, best of 3: 2.09 msec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.FJ(l)' 
1000 loops, best of 3: 725 usec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.JonClements(l)' 
1000 loops, best of 3: 617 usec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.moooeeeep(l)' 
100 loops, best of 3: 1.85 msec per loop

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


4
Мда - дивлячись на ранні справжні терміни - чи не 0.446найшвидший?
Джон Клементс

2
@JonClements, тому я написав більшість , зробив це зрозумілішим зараз. (більшість опублікованих, а не більшість перевірених ...)
moooeeeep

1
Я підозрюю, що JonClement настільки швидкий, тому що більша частина anyреалізована на
мові

1
+1 для вашого початкового рядка. Усі відповіді на sumнасправді гірші, ніж простий і прямий код ОП.
Вім,

2
@MarkAmery Я додав як розділ про читаність та елегантність (правда, короткий), так і про оцінку ефективності. Оскільки питання, що задається для розумності, я думаю, обидва аспекти повинні бути предметом розгляду. Як я бачу, я дав відповідь, щоб розглянути обидва ці відповідні аспекти. Якщо ви вважаєте, що ця відповідь не корисна, сміливо голосуйте проти.
moooeeeep

259

Той, який не вимагає імпорту:

def single_true(iterable):
    i = iter(iterable)
    return any(i) and not any(i)

Як варіант, можливо, більш читабельна версія:

def single_true(iterable):
    iterator = iter(iterable)

    # consume from "i" until first true or it's exhausted
    has_true = any(iterator) 

    # carry on consuming until another true value / exhausted
    has_another_true = any(iterator) 

    # True if exactly one true found
    return has_true and not has_another_true

Це:

  • Прагне переконатися, що iмає якесь справжнє значення
  • Продовжує дивитися з цього пункту на ітерабель, щоб переконатися, що немає іншого справжнього значення

34
@MatthewScouten ні ... ми споживаємо ітерацію тут ... спробуйте запустити код ...
Джон Клементс

12
@MatthewScouten відповідно до споживання ітераційного. anyбуде згідно з документами повертати True, як тільки буде знайдено неправдиве значення. Після цього ми знову шукаємо справжнє значення, і якщо його знайдемо, розглянемо його як помилку ... Тож це буде працювати для порожніх списків, списків / інших послідовностей та будь-яких ітеративних ...
Джон Клементс

12
@MathewScouten Побічні ефекти порушують усі теореми! x and not x = Falseє правильним, лише якщо xє посилально прозорим.
Бен,

14
@wim Це не деталь реалізації any()- це задокументована особливість функції та гарантована функціональність будь-якої реалізації, яка відповідає специфікації Python.
Гарет Латті,

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

49

Це залежить від того, чи ви просто шукаєте значення, Trueчи також шукаєте інші значення, які оцінювали б Trueлогічно (як 11або "hello"). Якщо перший:

def only1(l):
    return l.count(True) == 1

Якщо останнє:

def only1(l):
    return sum(bool(e) for e in l) == 1

оскільки це робило б і підрахунок, і перетворення в одній ітерації без необхідності створювати новий список.


2
У Python 3:list(map(bool, l)).count(True)
тикай

Це знаходить лише буквальне значення True, а не інші істинні значення (тобто: позитивні ints, не порожні контейнери тощо)
Matthew Scouten

6
Тільки для того, щоб вказати OP, це, швидше за все, не призведе до короткого замикання, коли буде знайдено більше одного значення "True", тому їх код може дати їм більшу ефективність за певних обставин.
NominSim

2
Другу функцію можна записати як return sum(bool(e) for e in l) == 1. boolпідкласи intта True / False поводяться як 1/0 щодо арифметики.

1
Я б уникав використання lв якості імені змінної (це занадто схоже на 1тут), і я б переписав sum(bool(e) for e in l)якsum(1 for e in l if e)
wim

22

Однорядкова відповідь, яка зберігає поведінку короткого замикання:

from itertools import ifilter, islice

def only1(l):
    return len(list(islice(ifilter(None, l), 2))) == 1

Це буде значно швидше, ніж інші альтернативи тут для дуже великих ітерацій, які мають дві або більше справжніх величин порівняно рано.

ifilter(None, itr)дає ітерацію, яка дасть лише неправдиві елементи ( xє справжньою, якщо bool(x)повертається True). islice(itr, 2)дає ітерацію, яка дасть лише перші два елементи itr. Перетворивши це в список і перевіривши, що довжина дорівнює одиниці, ми можемо перевірити, що існує точно один істинний елемент, не потребуючи перевірки будь-яких додаткових елементів після того, як ми знайшли два.

Ось кілька порівнянь часу:

  • Код налаштування:

    In [1]: from itertools import islice, ifilter
    
    In [2]: def fj(l): return len(list(islice(ifilter(None, l), 2))) == 1
    
    In [3]: def david(l): return sum(bool(e) for e in l) == 1
    
  • Показ поведінки короткого замикання:

    In [4]: l = range(1000000)
    
    In [5]: %timeit fj(l)
    1000000 loops, best of 3: 1.77 us per loop
    
    In [6]: %timeit david(l)
    1 loops, best of 3: 194 ms per loop
    
  • Великий список, де не відбувається короткого замикання:

    In [7]: l = [0] * 1000000
    
    In [8]: %timeit fj(l)
    100 loops, best of 3: 10.2 ms per loop
    
    In [9]: %timeit david(l)
    1 loops, best of 3: 189 ms per loop
    
  • Невеликий список:

    In [10]: l = [0]
    
    In [11]: %timeit fj(l)
    1000000 loops, best of 3: 1.77 us per loop
    
    In [12]: %timeit david(l)
    1000000 loops, best of 3: 990 ns per loop
    

Отже, sum()підхід є швидшим для дуже маленьких списків, але, оскільки список введення стає більшим, моя версія стає швидшою, навіть коли коротке замикання неможливе. Коли можливе коротке замикання на великому вході, різниця в продуктивності очевидна.


5
Ой Мені довелося тричі довше, ніж зрозуміти інші варіанти. Якщо коротке замикання важливо, я б взяв код OP, оскільки він набагато очевидніший і приблизно такий же ефективний.

1
Голосуйте за стиль і зберігайте коротке замикання. Але це важче для читання.
Метью Скаутен,

1
+1. Єдиний, який відтворює повний намір ОП щодо короткого замикання.
NominSim

1
+1, якщо ви проводите timeitексперименти для об’єктивного порівняння ефективності з рішенням OP.
moooeeeep

@moooeeeep Наївно, якщо у вас був нескінченний ітератор, який має два Trueзначення десь "рано", це закінчиться, порівняно з іншими відповідями, які вічно крутяться, намагаючись отримати рахунок.
NominSim

15

Я хотів отримати значок некроманта, тому узагальнив чудову відповідь Джона Клементса, зберігаючи переваги логіки короткого замикання та швидкої перевірки предикатів із усіма.

Отже ось:

N (вірно) = n

def n_trues(iterable, n=1):
    i = iter(iterable)
    return all(any(i) for j in range(n)) and not any(i)

N (вірно) <= n:

def up_to_n_trues(iterable, n=1):
    i = iter(iterable)
    all(any(i) for j in range(n))
    return not any(i)

N (вірно)> = n:

def at_least_n_trues(iterable, n=1):
    i = iter(iterable)
    return all(any(i) for j in range(n))

m <= N (вірно) <= n

def m_to_n_trues(iterable, m=1, n=1):
    i = iter(iterable)
    assert m <= n
    return at_least_n_trues(i, m) and up_to_n_trues(i, n - m)

11
>>> l = [0, 0, 1, 0, 0]
>>> has_one_true = len([ d for d in l if d ]) == 1
>>> has_one_true
True

4
Чому це було проти? Я думаю, що це найпростіший та читабельніший з усіх.
дансальмо

1
@dansalmo: Звичайно, важко бути впевненим, але моя теорія полягає в тому, що багато програмістів n00b python - можливо, зокрема, з фоном Java - почуваються комфортніше з довшим синтаксисом. (Я сам колись був трохи таким 5-10 років тому, але сьогодні вважаю це непрофесійним та невігласним.) +1
Йонас Бистрем

5

Ви можете зробити:

x = [bool(i) for i in x]
return x.count(True) == 1

Або

x = map(bool, x)
return x.count(True) == 1

Спираючись на метод @ JoranBeasley:

sum(map(bool, x)) == 1

5
if sum([bool(x) for x in list]) == 1

(Припускаючи, що всі ваші значення логічні.)

Можливо, це було б швидше, просто підсумувавши це

sum(list) == 1   

хоча це може спричинити деякі проблеми залежно від типів даних у вашому списку.


1
Тут було б непогано використовувати великі літери та пунктуацію.
Стівен Румбальський


4

Це, здається, працює і повинно мати можливість обробляти будь-які ітерації, а не тільки lists. Це забезпечує коротке замикання, коли це можливо, щоб максимізувати ефективність. Працює як в Python 2, так і в 3.

def only1(iterable):
    for i, x in enumerate(iterable):  # check each item in iterable
        if x: break                   # truthy value found
    else:
        return False                  # no truthy value found
    for x in iterable[i+1:]:          # one was found, see if there are any more
        if x: return False            #   found another...
    return True                       # only a single truthy value found

testcases = [  # [[iterable, expected result], ... ]
    [[                          ], False],
    [[False, False, False, False], False],
    [[True,  False, False, False], True],
    [[False, True,  False, False], True],
    [[False, False, False, True],  True],
    [[True,  False, True,  False], False],
    [[True,  True,  True,  True],  False],
]

for i, testcase in enumerate(testcases):
    correct = only1(testcase[0]) == testcase[1]
    print('only1(testcase[{}]): {}{}'.format(i, only1(testcase[0]),
                                             '' if correct else
                                             ', error given '+str(testcase[0])))

Вихід:

only1(testcase[0]): False
only1(testcase[1]): False
only1(testcase[2]): True
only1(testcase[3]): True
only1(testcase[4]): True
only1(testcase[5]): False
only1(testcase[6]): False

Мені подобається такий підхід, як щодо переробки логіки навколо, iter(x for x in my_list if x)а потім використання next, можливо, приємніше, ніж використання mapтаlist.index
wim

@wim: Хоча я не використовував запропонований вами підхід, ваш коментар надихнув мене переглянути свою оригінальну відповідь та зробити її ще більш природною і позбутися від mapі list.index.
мартіно

3

Рішення @ JonClements розширено не більше ніж для N True значень :

# Extend any() to n true values
def _NTrue(i, n=1):
    for x in xrange(n):
        if any(i): # False for empty
            continue
        else:
            return False
    return True

def NTrue(iterable, n=1):
    i = iter(iterable)
    return any(i) and not _NTrue(i, n)

редагувати: краща версія

def test(iterable, n=1): 
    i = iter(iterable) 
    return sum(any(i) for x in xrange(n+1)) <= n 

edit2: включіть принаймні m True і щонайбільше n True

def test(iterable, n=1, m=1): 
    i = iter(iterable) 
    return  m <= sum(any(i) for x in xrange(n+1)) <= n

1
Ні, я маю на увазі максимум. Вона повертає істину , якщо не більше N існує справжнє багатозначне значення: наприклад , 3 траса в списку тисячу отримає iterable.count(True) = 3, NTrue(iterable, 1) = False, NTrue(iterable, 2) = False, NTrue(iterable, 3) = True, NTrue(iterable, 4) = True, ... Це в основному поширюється на and not any(i)частину доand not any(i) and not any(i) and not...
Nisan.H

1
Тут не all(any(i) for i in xrange(n)) and not any(i)працює?
Ерік

@Eric що б повертати істину лише для точності п справжніх х. Це все-таки дало мені ідею підсумувати anys, однак.
Nisan. H

Ви скоріше маєте на увазі any(i) and not all(any(i) for x in xrange(n))?
moooeeeep

@moooeeeep Чи не True and not all(<n booleans>)логічно це те саме, що count(True) <= n? Ідея все ж полягає в тестуванні найменшого можливого набору та розриву при першій умові відмови.
Nisan.H

2
def only1(l)
    sum(map(lambda x: 1 if x else 0, l)) == 1

Пояснення: mapФункція відображає список в інший список, виконуючи True => 1та False => 0. Тепер у нас є список 0 і 1, замість True або False. Тепер ми просто підсумовуємо цей список, і якщо він дорівнює 1, було лише одне значення True.


1

Це те, що ви шукаєте?

sum(l) == 1

Для списку це не вдається: [2], оскільки автор не вказав, що елементи повинні бути лише True і False, або 1 і 0
vtlinh

1

Для повноти та для демонстрації розширеного використання потоку управління Python для ітерації циклу можна уникнути зайвого обліку у прийнятій відповіді, роблячи це трохи швидше:

def one_bool_true(iterable):
    it = iter(iterable)
    for i in it:
        if i:
            break
    else:            #no break, didn't find a true element
        return False
    for i in it:     # continue consuming iterator where left off
        if i: 
            return False
    return True      # didn't find a second true.

Вище просто потік управління дозволяє використовувати складні функції в Python петель: else. Семантика полягає в тому, що якщо ви закінчите ітерацію над ітератором, який ви споживаєте, не breakвиходячи з нього, ви вводите elseблок.

Ось прийнята відповідь, яка використовує трохи більше обліку.

def only1(l):
    true_found = False
    for v in l:
        if v:
            # a True was found!
            if true_found:
                # found too many True's
                return False 
            else:
                # found the first True
                true_found = True
    # found zero or one True value
    return true_found

на час:

import timeit
>>> min(timeit.repeat(lambda: one_bool_true([0]*100 + [1, 1])))
13.992251592921093
>>> min(timeit.repeat(lambda: one_bool_true([1, 1] + [0]*100)))
2.208037032979064
>>> min(timeit.repeat(lambda: only1([0]*100 + [1, 1])))
14.213872335107908
>>> min(timeit.repeat(lambda: only1([1, 1] + [0]*100)))
2.2482982632641324
>>> 2.2482/2.2080
1.0182065217391305
>>> 14.2138/13.9922
1.0158373951201385

Отже, ми бачимо, що прийнята відповідь займає трохи більше часу (трохи більше півтора відсотка).

Природно, що використання вбудованого any, написаного на C, набагато швидше (див. Відповідь Джона Клемента щодо реалізації - це коротка форма):

>>> min(timeit.repeat(lambda: single_true([0]*100 + [1, 1])))
2.7257133318785236
>>> min(timeit.repeat(lambda: single_true([1, 1] + [0]*100)))
2.012824866380015

0
import collections

def only_n(l, testval=True, n=1):
    counts = collections.Counter(l)
    return counts[testval] == n

Лінійний час. Використовує вбудований клас Counter, який вам слід використовувати для перевірки підрахунку.

Перечитуючи своє запитання, схоже, ви насправді хочете перевірити, чи є лише одне неправдиве значення, а не одне Trueзначення. Спробуйте це:

import collections

def only_n(l, testval=True, coerce=bool, n=1):
    counts = collections.Counter((coerce(x) for x in l))
    return counts[testval] == n

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

Ось версія, оптимізована для найкращої роботи:

import collections
import itertools

def only_n(l, testval=True, coerce=bool, n=1):
    counts = collections.Counter()
    def iterate_and_count():
        for x in itertools.imap(coerce,l):
            yield x
            if x == testval and counts[testval] > n:
               break
    counts.update(iterate_and_count())
    return counts[testval] == n

Найгірший результат має високий рівень k(як і в O(kn+c)), але він є загальним.

Ось ідеон для експериментів з продуктивністю: http://ideone.com/ZRrv2m


0

Ось щось, що мало б працювати на будь-що неправдиве, хоча воно не має короткого замикання. Я знайшов це, шукаючи чистий спосіб заборонити взаємовиключні аргументи:

if sum(1 for item in somelist if item) != 1:
    raise ValueError("or whatever...")

0

Що стосовно:

len([v for v in l if type(v) == bool and v])

Якщо потрібно лише підрахувати логічні значення True.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.