Jak mogę sprawdzić, czy lista ma jedną i tylko jedną prawdziwą wartość?

83

W Pythonie mam listę, która powinna mieć jedną i tylko jedną prawdziwą wartość (to znaczy bool(value) is True). Czy istnieje sprytny sposób, aby to sprawdzić? W tej chwili tylko iteruję listę i ręcznie sprawdzam:

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

Wydaje się to nieeleganckie i niezbyt pytoniczne. Czy jest na to sprytniejszy sposób?

Matthew Scouten
źródło
2
Myślę, że twoje rozwiązanie jest całkiem dobre i jest pytoniczne!
wim
1
Common Lisp: (= 1 (count-if #'identity list)).
Kaz
7
sum(lst) == 1
Pål GD
Żeby było jasne: czy chcesz sprawdzić, czy istnieje Truetylko jedna prawdziwa wartość, czy tylko jedna?
Marcin

Odpowiedzi:

44

Najbardziej rozwlekłe rozwiązanie nie zawsze jest najbardziej nieeleganckie. Dlatego dodaję tylko niewielką modyfikację (w celu zaoszczędzenia kilku zbędnych ocen logicznych):

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

Oto kilka czasów do porównania:

# 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

Mój wynik:

$ 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

Jak widać, rozwiązanie OP jest znacznie lepsze niż większość innych zamieszczonych tutaj rozwiązań. Zgodnie z oczekiwaniami najlepsze są te z zachowaniem zwarciowym, zwłaszcza to rozwiązanie opublikowane przez Jona Clementsa. Przynajmniej w przypadku dwóch wczesnych Truewartości na długiej liście.

Tutaj to samo dla żadnej Truewartości:

$ 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

Nie sprawdzałem istotności statystycznej, ale co ciekawe, tym razem podejścia sugerowane przez FJ, a zwłaszcza to Jona Clementsa, ponownie wydają się wyraźnie lepsze.

moooeeeep
źródło
4
Umm - patrząc na wczesne, prawdziwe czasy - czy nie jest 0.446to najszybsze?
Jon Clements
2
@JonClements dlatego pisałem najwięcej , teraz wyjaśniłem to bardziej. (większość
postów
1
Podejrzewam, że JonClement jest tak szybki, ponieważ większość z nich anyjest zaimplementowana w języku C
Matthew Scouten
1
+1 za twoją pierwszą linię. Wszystkie odpowiedzi sumsą w rzeczywistości gorsze niż prosty i bezpośredni kod PO ..
wim
2
@MarkAmery Dodałem zarówno sekcję o czytelności i elegancji (wprawdzie krótką), jak i o ocenie wydajności. Wydaje mi się, że w kwestii sprytu należy rozważyć oba aspekty. Jak widzę, udzieliłem odpowiedzi, aby odnieść się do obu tych istotnych aspektów. Jeśli uważasz, że ta odpowiedź nie jest przydatna, możesz głosować przeciw.
moooeeeep,
259

Taki, który nie wymaga importu:

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

Alternatywnie, być może bardziej czytelna wersja:

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

To:

  • Wygląda na to, że ima jakąkolwiek prawdziwą wartość
  • Wciąż patrzy od tego punktu w iterowalne, aby upewnić się, że nie ma innej prawdziwej wartości
Jon Clements
źródło
34
@MatthewScouten no ... konsumujemy z iterowalnego tutaj ... spróbuj uruchomić kod ...
Jon Clements
12
@MatthewScouten zgodnie z konsumpcją iterowalnej. anybędzie zgodnie z dokumentami zwróci True, gdy tylko zostanie znaleziona wartość inna niż fałszywa. Następnie ponownie szukamy prawdziwej wartości, a jeśli zostanie znaleziona, traktujemy ją jako błąd ... Więc to zadziała dla pustych list, list / innych sekwencji i wszelkich iterowalnych ...
Jon Clements
12
@MathewScouten Efekty uboczne łamią wszystkie twierdzenia! x and not x = Falsejest poprawne tylko wtedy, gdy xjest referencyjnie przezroczyste.
Ben
14
@wim To nie jest szczegół implementacji any()- to udokumentowana cecha funkcji i gwarantowana funkcjonalność dowolnej implementacji, która jest zgodna ze specyfikacją Pythona.
Gareth Latty
17
Każdy, kto uważa, że ​​nie jest to czytelne rozwiązanie, powinien rozważyć to: jest zwięzłe i opiera się tylko na znanych zachowaniach i typowej konstrukcji Pythona. Tylko dlatego, że noob by tego nie zrozumiał, nie czyni go czytelnym. Jest to również doskonały sposób na nauczanie tego, co należy wiedzieć, ponieważ budzi natychmiastową ciekawość u tych, którzy nie widzą, jak to działa.
dansalmo,
49

Zależy to od tego, czy szukasz tylko wartości, Trueczy też szukasz innych wartości, które można by ocenić Truelogicznie (jak 11lub "hello"). Jeśli pierwszy:

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

Jeśli ten ostatni:

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

ponieważ spowodowałoby to zliczanie i konwersję w jednej iteracji, bez konieczności tworzenia nowej listy.

David Robinson
źródło
2
W Pythonie 3:list(map(bool, l)).count(True)
poke
To tylko znajduje dosłowne True, a nie inne prawdziwe wartości (np .: dodatnie wartości int, a nie puste pojemniki itp.)
Matthew Scouten
6
Wystarczy wskazać OP, że prawdopodobnie nie spowoduje to zwarcia, gdy zostanie znalezionych więcej niż jedna wartość „Prawda”, więc ich kod może zapewnić większą wydajność w pewnych okolicznościach.
NominSim
2
Drugą funkcję można zapisać jako return sum(bool(e) for e in l) == 1. boolpodklasy inti Prawda / Fałsz zachowują się jak 1/0 w zakresie arytmetyki.
1
Unikałbym używania ljako nazwy zmiennej (wygląda zbyt podobnie jak 1tutaj) i przepisałbym sum(bool(e) for e in l)jakosum(1 for e in l if e)
wim
22

Jednowierszowa odpowiedź, która zachowuje zachowanie polegające na zwarciu:

from itertools import ifilter, islice

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

Będzie to znacznie szybsze niż inne alternatywy tutaj dla bardzo dużych iteracji, które mają dwie lub więcej wartości rzeczywistych stosunkowo wcześnie.

ifilter(None, itr)daje iterowalne, które dostarczą tylko prawdziwych elementów ( xjest prawdziwe, jeśli bool(x)zwraca True). islice(itr, 2)daje element iterowalny, który zwróci tylko pierwsze dwa elementy itr. Konwertując to na listę i sprawdzając, czy długość jest równa jeden, możemy sprawdzić, czy istnieje dokładnie jeden prawdziwy element bez konieczności sprawdzania jakichkolwiek dodatkowych elementów po znalezieniu dwóch.

Oto kilka porównań czasowych:

  • Kod konfiguracji:

    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
    
  • Wykazujące zachowanie zwarciowe:

    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
    
  • Duża lista, w których nie występuje zwarcie:

    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
    
  • Mała lista:

    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
    

Więc sum()podejście jest szybsze w przypadku bardzo małych list, ale gdy lista wejściowa staje się większa, moja wersja jest szybsza nawet wtedy, gdy zwarcie nie jest możliwe. Gdy zwarcie jest możliwe na dużym wejściu, różnica w wydajności jest wyraźna.

Andrew Clark
źródło
5
Auć. Trzykrotnie zajęło mi to zrozumienie innych opcji. Jeśli zwarcie jest ważne, wziąłbym kod OP, ponieważ jest znacznie bardziej oczywisty i mniej więcej tak samo wydajny.
1
Głosuj za stylem i zachowując zwarcie. Ale trudniej to przeczytać.
Matthew Scouten
1
+1. Jedyny, który w pełni odtworzył zamiar OP, jakim jest zwarcie.
NominSim
1
+1, jeśli zapewnisz trochę timeiteksperymentów w celu obiektywnego porównania wydajności z rozwiązaniem OP.
moooeeeep
@moooeeeep Naiwnie, gdybyś miał nieskończoną iterowalność, która ma Truegdzieś „wcześnie” dwie wartości, to się skończy, w przeciwieństwie do innych odpowiedzi, które zawsze kręcą kołami, próbując policzyć.
NominSim
15

Chciałem zdobyć odznakę nekromanty, więc uogólniłem doskonałą odpowiedź Jona Clementsa, zachowując zalety logiki zwarciowej i szybkiego sprawdzania predykatów ze wszystkimi.

Oto więc:

N (prawda) = n

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

N (prawda) <= 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 (prawda)> = n:

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

m <= N (prawda) <= 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)
Antti Haapala
źródło
11
>>> l = [0, 0, 1, 0, 0]
>>> has_one_true = len([ d for d in l if d ]) == 1
>>> has_one_true
True
gariel
źródło
4
Dlaczego ten głos został odrzucony? Myślę, że jest to najprostsze i najbardziej czytelne ze wszystkich.
dansalmo
1
@dansalmo: Trudno być oczywiście pewnym, ale moja teoria jest taka, że ​​wielu programistów n00b w Pythonie - być może szczególnie ci z językiem Java - czuje się bardziej komfortowo z dłuższą składnią. (Ja byłem trochę taki 5-10 lat temu, ale dziś uważam to za nieprofesjonalne i ignoranckie.) +1
Jonas Byström
5

Możesz to zrobić:

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

Lub

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

Opierając się na metodzie @ JoranBeasley:

sum(map(bool, x)) == 1
karthikr
źródło
5
if sum([bool(x) for x in list]) == 1

(Zakładając, że wszystkie twoje wartości są logiczne).

Prawdopodobnie byłoby to szybsze po prostu zsumowanie

sum(list) == 1   

chociaż może to powodować pewne problemy w zależności od typów danych na liście.

Joran Beasley
źródło
1
Przydałyby się tutaj wielkie litery i znaki interpunkcyjne.
Steven Rumbalski
4

Jeśli jest tylko jeden True, długość Trues powinna wynosić jeden:

def only_1(l): return 1 == len(filter(None, l))
Marc Laugharn
źródło
2
Może mógłbyś wyjaśnić swoją odpowiedź?
Linus Caldwell
4

Wydaje się, że to działa i powinno być w stanie obsłużyć każdą iterację, nie tylko lists. W miarę możliwości zwiera, aby zmaksymalizować wydajność. Działa zarówno w Pythonie 2, jak i 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])))

Wynik:

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
martineau
źródło
Podoba mi się to podejście, co powiesz na przerobienie logiki iter(x for x in my_list if x)i użycie next, może ładniejszego niż używanie mapilist.index
wim
@wim: Chociaż nie zastosowałem zaproponowanego przez Ciebie podejścia, Twój komentarz zainspirował mnie do zrewidowania mojej pierwotnej odpowiedzi i uczynienia jej jeszcze bardziej przyrostową oraz pozbycia się mapi list.index.
martineau
3

Rozwiązanie @ JonClements zostało rozszerzone na co najwyżej N wartości 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)

edycja: lepsza wersja

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

edit2: uwzględnij co najmniej m True i co najwyżej n True

def test(iterable, n=1, m=1): 
    i = iter(iterable) 
    return  m <= sum(any(i) for x in xrange(n+1)) <= n
Nisan H.
źródło
1
Nie, mam na myśli najwyżej. Zwraca True jeżeli istnieją co najwyżej N wartościami rzeczywistymi wartościami: np 3 Trues na liście 1000 dostanie iterable.count(True) = 3, NTrue(iterable, 1) = False, NTrue(iterable, 2) = False, NTrue(iterable, 3) = True, NTrue(iterable, 4) = True, ... to w zasadzie rozszerza and not any(i)część doand not any(i) and not any(i) and not...
Nisan.H
1
Nie all(any(i) for i in xrange(n)) and not any(i)działa tutaj?
Eric
@Eric, który zwróci True tylko dla dokładnie n wartości prawdziwych. Dało mi to jednak pomysł, aby zsumować wartości anys.
Nisan.H
Masz na myśli raczej any(i) and not all(any(i) for x in xrange(n))?
moooeeeep
@moooeeeep True and not all(<n booleans>)logicznie nie jest tym samym, co count(True) <= n? Chodzi o to, aby przetestować najmniejszy możliwy zestaw i przerwać w pierwszym stanie awarii.
Nisan.H
2
def only1(l)
    sum(map(lambda x: 1 if x else 0, l)) == 1

Objaśnienie: mapFunkcja odwzorowuje listę na inną listę, wykonując czynności True => 1i False => 0. Mamy teraz listę zer i jedynek zamiast Prawda lub Fałsz. Teraz po prostu zsumujemy tę listę i jeśli wynosi 1, była tylko jedna wartość True.

Martin Konecny
źródło
1

Czy tego szukasz?

sum(l) == 1
c-urchin
źródło
Nie udaje się to w przypadku listy: [2], ponieważ autor nie określił, że elementy muszą być tylko prawdziwe i fałszywe, lub 1 i 0
vtlinh
1

Ze względu na kompletność i aby zademonstrować zaawansowane wykorzystanie przepływu sterowania Pythona dla iteracji pętli, można uniknąć dodatkowego rozliczania w akceptowanej odpowiedzi, dzięki czemu jest to nieco szybsze .:

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.

Powyższe proste przepływ sterowania wykorzystuje Pythona wyrafinowanych funkcji pętli: the else. Semantyka polega na tym, że jeśli zakończysz iterację po iteratorze, który konsumujesz, bez breakwychodzenia z niego, wtedy wchodzisz do elsebloku.

Oto akceptowana odpowiedź, która wymaga nieco większej rachunkowości.

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

do czasu te:

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

Widzimy więc, że zaakceptowana odpowiedź trwa nieco dłużej (nieco ponad półtora procenta).

Oczywiście korzystanie z wbudowanego any, napisanego w C, jest znacznie szybsze (patrz odpowiedź Jona Clementa na implementację - to jest krótka forma):

>>> min(timeit.repeat(lambda: single_true([0]*100 + [1, 1])))
2.7257133318785236
>>> min(timeit.repeat(lambda: single_true([1, 1] + [0]*100)))
2.012824866380015
Aaron Hall
źródło
0
import collections

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

Czas liniowy. Używa wbudowanej klasy Counter, której należy używać do sprawdzania zliczeń.

Po ponownym przeczytaniu pytania wygląda na to, że faktycznie chcesz sprawdzić, czy jest tylko jedna prawdziwa wartość, a nie jedna Truewartość. Spróbuj tego:

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

Chociaż możesz uzyskać lepszą wydajność w najlepszym przypadku, nic nie ma lepszej wydajności w najgorszym przypadku. Jest to również krótkie i łatwe do odczytania.

Oto wersja zoptymalizowana pod kątem wydajności w najlepszym przypadku:

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

W najgorszym przypadku wydajność jest wysoka k(jak w O(kn+c)), ale jest całkowicie ogólna.

Oto pomysł na eksperymentowanie z wydajnością: http://ideone.com/ZRrv2m

Marcin
źródło
0

Oto coś, co powinno działać na wszystko, co prawda, chociaż nie ma zwarcia. Znalazłem to, szukając czystego sposobu na zakazanie wzajemnie wykluczających się argumentów:

if sum(1 for item in somelist if item) != 1:
    raise ValueError("or whatever...")
Andrzej
źródło
0

Co powiesz na:

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

Jeśli chcesz liczyć tylko wartości logiczne True.

Radek Svoboda
źródło