Как я могу проверить, что список имеет одно и только одно истинное значение?

83

В 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)).
Kaz
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них реализована на C
Мэтью Скоутен,
1
+1 За вашу первую строчку. Все ответы с sumна самом деле хуже, чем простой и прямой код OP ..
wim
2
@MarkAmery Я добавил раздел по удобочитаемости и элегантности (правда, короткий) и по оценке производительности. Поскольку вопрос, заданный для умения, я думаю, следует рассмотреть оба аспекта. На мой взгляд, я дал ответ на оба этих важных аспекта. Если вы считаете, что этот ответ бесполезен, не стесняйтесь голосовать против.
moooeeeep 08
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 07
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, а не другие истинные значения (например: положительные целые числа, а не пустые контейнеры и т. Д.)
Мэтью Скоутен
6
Просто чтобы указать OP, это, скорее всего, не приведет к короткому замыканию, когда будет найдено более одного значения «Истина», поэтому их код может дать им большую эффективность в определенных обстоятельствах.
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
Почему это было отклонено? Думаю, это самый простой и читаемый из всех.
dansalmo
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

Если есть только один True, то длина Trues должна быть равна единице:

def only_1(l): return 1 == len(filter(None, l))
Марк Лохарн
источник
2
Может ты объяснишь свой ответ?
Линус Колдуэлл
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 истинных значений :

# 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 истинных и не более n истинных

def test(iterable, n=1, m=1): 
    i = iter(iterable) 
    return  m <= sum(any(i) for x in xrange(n+1)) <= n
Nisan.H
источник
1
Нет, самое большее. Она возвращает истину , если не более N существует истинное многозначное значение: например , 3 траса в списке 1000 получит 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 что бы возвращать истину лишь для точности п истинных х. anyТем не менее, это натолкнуло меня на мысль суммировать по s.
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 06
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
c-urchin
источник
Это не подходит для списка: [2], поскольку автор не указал, что элементы должны быть только True и False или 1 и 0
vtlinh
1

Для полноты картины и демонстрации расширенного использования потока управления Python для итерации цикла for можно избежать лишнего учета в принятом ответе, что сделает это немного быстрее:

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.

Радек Свобода
источник