Sintaxe Python para "se a ou b ou c, mas não todos eles"

130

Eu tenho um script python que pode receber zero ou três argumentos de linha de comando. (Ele é executado no comportamento padrão ou precisa dos três valores especificados.)

Qual é a sintaxe ideal para algo como:

if a and (not b or not c) or b and (not a or not c) or c and (not b or not a):

?

Chris Wilson
fonte
4
talvez comece com algo como `if len (sys.argv) == 0:
Edgar Aroutiounian
6
@EdgarAroutiounian len(sys.argv)sempre será pelo menos 1: inclui o executável como argv[0].
RoadieRich
10
O corpo da pergunta não corresponde ao título da pergunta. Deseja marcar "se a ou b ou c, mas não todos eles" ou "se exatamente um de a, bec" (como a expressão que você deu)?
Doug McClean
2
O que você pode dizer sobre a + b + c?
gukoff
6
Espere, pergunta, pode levar zero ou três argumentos. você não poderia simplesmente dizer if not (a and b and c)(zero args) e depois if a and b and c(todos os três args)?
acolyte 13/05

Respostas:

236

Se você quer dizer um formulário mínimo, vá com isso:

if (not a or not b or not c) and (a or b or c):

O que traduz o título da sua pergunta.

UPDATE: como corretamente dito pela Volatility e Supr, você pode aplicar a lei de De Morgan e obter o equivalente:

if (a or b or c) and not (a and b and c):

Meu conselho é usar o formulário que for mais significativo para você e para outros programadores. O primeiro significa "há algo falso, mas também algo verdadeiro" , o segundo "há algo verdadeiro, mas não tudo" . Se eu otimizasse ou fizesse isso no hardware, escolheria o segundo, aqui, apenas o mais legível (também levando em consideração as condições que você testará e seus nomes). Eu escolhi o primeiro.

Stefano Sanfilippo
fonte
3
Todas ótimas respostas, mas isso ganha por concisão, com ótimos curtos-circuitos. Obrigado a todos!
22613 Chris Wilson
38
Eu torná-lo ainda mais conciso e ir comif not (a and b and c) and (a or b or c)
Volatilidade
208
Ou até mesmo if (a or b or c) and not (a and b and c)para combinar perfeitamente com o título;) #
13/13
3
@ HennyH Eu acredito que a pergunta pede "pelo menos uma condição verdadeira, mas não todas", não "apenas uma condição verdadeira".
Volatilidade
63
@Suprif any([a,b,c]) and not all([a,b,c])
eternalmatt
238

E se:

conditions = [a, b, c]
if any(conditions) and not all(conditions):
   ...

Outra variante:

if 1 <= sum(map(bool, conditions)) <= 2:
   ...
desfazer
fonte
2
sum(conditions)pode dar errado se algum deles retornar, 2por exemplo, o que é True.
Eumiro 13/05
7
Verdade que você precisaria de um feiosum(map(bool, conditions))
jamylak
5
Observe que isso não está em curto-circuito, porque todas as condições são pré-avaliadas.
Georg
14
@PaulScheltema A primeira forma é mais compreensível para qualquer pessoa.
Cmh
6
Este "qualquer e nem todos" é o melhor e mais clara dos métodos boolean, basta ser consciente da importante distinção entre um arg estar presente e um arg ser 'truthy'
Wim
115

Essa pergunta já tinha muitas respostas altamente votadas e uma resposta aceita, mas todas elas até agora foram distraídas por várias maneiras de expressar o problema booleano e perderam um ponto crucial:

Eu tenho um script python que pode receber zero ou três argumentos de linha de comando. (Ele é executado no comportamento padrão ou precisa dos três valores especificados)

Essa lógica não deve ser de responsabilidade do seu código em primeiro lugar , mas deve ser manipulada porargparsemódulo. Não se preocupe em escrever uma declaração if complexa, em vez disso, prefira configurar seu analisador de argumentos da seguinte forma:

#!/usr/bin/env python
import argparse as ap
parser = ap.ArgumentParser()
parser.add_argument('--foo', nargs=3, default=['x', 'y', 'z'])
args = parser.parse_args()
print(args.foo)

E sim, deve ser uma opção, não um argumento posicional, porque afinal é opcional .


editado: Para abordar a preocupação de LarsH nos comentários, abaixo está um exemplo de como você pode escrevê-lo se tiver certeza de que deseja a interface com 3 ou 0argumentos posicionais . Sou da opinião de que a interface anterior tem um estilo melhor, porqueargumentos opcionais devem ser opções , mas aqui está uma abordagem alternativa por uma questão de integridade. Observe o kwarg sobrescritousageao criar seu analisador, pois,argparsecaso contrário,será gerada automaticamente uma mensagem de uso enganosa!

#!/usr/bin/env python
import argparse as ap
parser = ap.ArgumentParser(usage='%(prog)s [-h] [a b c]\n')
parser.add_argument('abc', nargs='*', help='specify 3 or 0 items', default=['x', 'y', 'z'])
args = parser.parse_args()
if len(args.abc) != 3:
  parser.error('expected 3 arguments')
print(args.abc)

Aqui estão alguns exemplos de uso:

# default case
wim@wim-zenbook:/tmp$ ./three_or_none.py 
['x', 'y', 'z']

# explicit case
wim@wim-zenbook:/tmp$ ./three_or_none.py 1 2 3
['1', '2', '3']

# example failure mode
wim@wim-zenbook:/tmp$ ./three_or_none.py 1 2 
usage: three_or_none.py [-h] [a b c]
three_or_none.py: error: expected 3 arguments
wim
fonte
4
Sim, eu adicionei isso intencionalmente. Seria possível tornar o argumento posicional e impor que exatamente 3 ou 0 fossem consumidos, mas não seria uma boa CLI, portanto não a recomendei.
Wim
8
Problema separado. Você não acredita que seja uma boa CLI e pode argumentar sobre esse ponto, e o OP pode ser persuadido. Mas sua resposta se desvia da pergunta de maneira significativa o suficiente para que a mudança de especificação precise ser mencionada. Parece que você está dobrando as especificações para se ajustar à ferramenta disponível, sem mencionar a alteração.
LarsH 13/05
2
@ LarsH OK, adicionei um exemplo que se encaixa melhor com a interface original implícita na pergunta. Agora ele está dobrando a ferramenta para atender a especificação disponível ...;)
Wim
2
Esta é a única resposta que votei. +1 para responder à pergunta real .
Jonathon Reinhart
1
+1. A forma da CLI é uma questão tangencial importante, não completamente separada como outra pessoa disse. Votei na sua postagem assim como em outras - a sua fica na raiz do problema e oferece uma solução elegante, enquanto outras postam a pergunta literal. E os dois tipos de respostas são úteis e merecem +1.
Ben Lee
32

Eu iria para:

conds = iter([a, b, c])
if any(conds) and not any(conds):
    # okay...

Eu acho que isso deve causar um curto-circuito com bastante eficiência

Explicação

Ao criar condsum iterador, o primeiro uso de anyentrará em curto-circuito e deixará o iterador apontando para o próximo elemento, se algum item for verdadeiro; caso contrário, consumirá toda a lista e será False. O próximo anypega os itens restantes no iterável e garante que não existem outros valores verdadeiros ... Se houver, toda a declaração não pode ser verdadeira, portanto, não há um elemento único (portanto, curto-circuitos novamente). O último anyretornará Falseou esgotará o iterável e o será True.

nota: o acima verifica se apenas uma condição está definida


Se você deseja verificar se um ou mais itens, mas nem todos os itens estão definidos, pode usar:

not all(conds) and any(conds)
Jon Clements
fonte
5
Eu não entendo. É como se fosse verdadeiro e não verdadeiro. Ajude-me a entender.
rGil
1
@rGil: lê como "se algumas maçãs são vermelhas e outras não" - é o mesmo que dizer "algumas maçãs são vermelhas, mas nem todas".
Georg
2
Mesmo com explicações, não consigo entender o comportamento ... Com o [a, b, c] = [True, True, False]seu código não "imprime" False, enquanto o resultado esperado é True?
awesoon 13/05
6
Isso é bastante inteligente, MAS: eu usaria essa abordagem se você não soubesse quantas condições você tinha no início, mas para uma lista fixa e conhecida de condicionais, a perda de legibilidade simplesmente não vale a pena.
fofo
4
Isso não provoca curto-circuito. A lista é completamente construída antes de ser passada para iter. anye allconsumirá preguiçosamente a lista, é verdade, mas a lista já foi completamente avaliada quando você chegar lá!
Icktoofay
22

A sentença em inglês:

"Se a ou b ou c, mas não todos"

Traduz para esta lógica:

(a or b or c) and not (a and b and c)

A palavra "mas" geralmente implica uma conjunção, em outras palavras "e". Além disso, "todos eles" se traduz em uma conjunção de condições: essa condição e essa condição e outra condição. O "não" inverte toda a conjunção.

Eu não concordo que a resposta aceita. O autor deixou de aplicar a interpretação mais direta à especificação e deixou de aplicar a Lei de De Morgan para simplificar a expressão a menos operadores:

 not a or not b or not c  ->  not (a and b and c)

enquanto afirma que a resposta é uma "forma mínima".

Kaz
fonte
Na verdade, essa forma é mínima. É a forma mínima de PoS para a expressão.
Stefano Sanfilippo
10

Isso retorna Truese uma e apenas uma das três condições for True. Provavelmente o que você queria no seu código de exemplo.

if sum(1 for x in (a,b,c) if x) == 1:
eumiro
fonte
Não tão bonita quanto a resposta por @defuz
jamylak
10

Que tal: (condição única)

if (bool(a) + bool(b) + bool(c) == 1):

Observe, se você permitir duas condições também, poderá fazer isso

if (bool(a) + bool(b) + bool(c) in [1,2]):
Casimir et Hippolyte
fonte
1
Para o registro, a pergunta pede duas condições. Pelo menos um, mas nem todos = 1 de um ou dois de todos #
Marius Balčytis 14/05
IMHO você deve escrever o segundo como 1 <= bool(a) + bool(b) + bool(c) <= 2.
Reintegrar Monica
6

Para ser claro, você deseja tomar sua decisão com base em quanto dos parâmetros é TRUE lógico (no caso de argumentos de string - não está vazio)?

argsne = (1 if a else 0) + (1 if b else 0) + (1 if c else 0)

Então você tomou uma decisão:

if ( 0 < argsne < 3 ):
 doSth() 

Agora a lógica está mais clara.

Marinheiro Danubiano
fonte
5

E por que não apenas contá-los?

import sys
a = sys.argv
if len(a) = 1 :  
    # No arguments were given, the program name count as one
elif len(a) = 4 :
    # Three arguments were given
else :
    # another amount of arguments was given
Louis
fonte
5

Se você não se importa em ser um pouco enigmático, pode simular o 0 < (a + b + c) < 3que retornará truese você tiver entre uma e duas declarações verdadeiras e false se todas forem falsas ou nenhuma for falsa.

Isso também simplifica se você usar funções para avaliar os bools, pois apenas avalia as variáveis ​​uma vez, o que significa que você pode escrever as funções em linha e não precisa armazenar temporariamente as variáveis. (Exemplo:. 0 < ( a(x) + b(x) + c(x) ) < 3)

Spinno
fonte
4

A pergunta afirma que você precisa dos três argumentos (a e bec) ou nenhum deles (não (a ou b ou c))

Isto dá:

(a e bec) ou não (a ou b ou c)

Relaxando em Chipre
fonte
4

Pelo que entendi, você tem uma função que recebe 3 argumentos, mas se não o fizer, será executada no comportamento padrão. Como você não explicou o que deve acontecer quando 1 ou 2 argumentos são fornecidos, assumirei que ele deve simplesmente executar o comportamento padrão. Nesse caso, acho que você encontrará a seguinte resposta muito vantajosa:

def method(a=None, b=None, c=None):
    if all([a, b, c]):
        # received 3 arguments
    else:
        # default behavior

No entanto, se você deseja que 1 ou 2 argumentos sejam tratados de maneira diferente:

def method(a=None, b=None, c=None):
    args = [a, b, c]
    if all(args):
        # received 3 arguments
    elif not any(args):
        # default behavior
    else:
        # some args (raise exception?)

nota: Isso pressupõe que os Falsevalores " " não serão passados ​​para este método.

Inbar Rose
fonte
verificar o valor de verdade de um argumento é uma questão diferente de verificar se um argumento é presente ou ausente
wim
@ wim Então, está convertendo uma pergunta para se adequar à sua resposta. O módulo argparse não tem nada a ver com a pergunta, ele adiciona outra importação e, se o OP não planeja usar o argparse, ele não os ajudará. Além disso, se o "script" não for autônomo, mas um módulo ou uma função dentro de um conjunto maior de códigos, ele já poderá ter um analisador de argumentos, e essa função específica dentro desse script maior poderá ser padrão ou personalizada. Devido a informações limitadas do OP, não sei como o método deve agir, mas é seguro assumir que o OP não está passando bools.
Inbar Rose
A pergunta dizia explicitamente "Eu tenho um script python que pode receber zero ou três argumentos de linha de comando", não disse "Eu tenho uma função que recebe 3 argumentos". Como o módulo argparse é a maneira preferida de lidar com argumentos de linha de comando em python, ele automaticamente tem tudo a ver com a pergunta. Por fim, o python é "baterias incluídas" - não há nenhuma desvantagem em "adicionar outra importação" quando esse módulo faz parte das bibliotecas padrão.
Wim
@ wim A questão não é clara (o corpo não corresponde ao título, por exemplo). Eu acho que a pergunta não é clara o suficiente para que seja uma resposta válida para alguma interpretação dela.
Reintegrar Monica
2

Se você trabalha com um iterador de condições, pode ser lento o acesso. Mas você não precisa acessar cada elemento mais de uma vez e nem sempre precisa ler tudo. Aqui está uma solução que funcionará com geradores infinitos:

#!/usr/bin/env python3
from random import randint
from itertools import tee

def generate_random():
    while True:
        yield bool(randint(0,1))

def any_but_not_all2(s): # elegant
    t1, t2 = tee(s)
    return False in t1 and True in t2 # could also use "not all(...) and any(...)"

def any_but_not_all(s): # simple
    hadFalse = False
    hadTrue = False
    for i in s:
        if i:
            hadTrue = True
        else:
            hadFalse = True
        if hadTrue and hadFalse:
            return True
    return False


r1, r2 = tee(generate_random())
assert any_but_not_all(r1)
assert any_but_not_all2(r2)

assert not any_but_not_all([True, True])
assert not any_but_not_all2([True, True])

assert not any_but_not_all([])
assert not any_but_not_all2([])

assert any_but_not_all([True, False])
assert any_but_not_all2([True, False])
Janus Troelsen
fonte
0

Quando todo dado boolé True, ou quando todo dado boolé False...
todos são iguais um ao outro!

Portanto, basta encontrar dois elementos que sejam avaliados para bools diferentes
para saber que há pelo menos um Truee pelo menos um False.

Minha curta solução:

not bool(a)==bool(b)==bool(c)

Eu acredito que curto-circuito, porque AFAIK a==b==cé igual a==b and b==c.

Minha solução generalizada:

def _any_but_not_all(first, iterable): #doing dirty work
    bool_first=bool(first)
    for x in iterable:
        if bool(x) is not bool_first:
            return True
    return False

def any_but_not_all(arg, *args): #takes any amount of args convertable to bool
    return _any_but_not_all(arg, args)

def v_any_but_not_all(iterable): #takes iterable or iterator
    iterator=iter(iterable)
    return _any_but_not_all(next(iterator), iterator)

Também escrevi um código que lida com várias iteráveis, mas a excluí daqui porque acho inútil. No entanto, ainda está disponível aqui .

GingerPlusPlus
fonte
-2

Isso é basicamente uma funcionalidade "alguns (mas não todos)" (quando contrastada com as funções any()e all()internas).

Isso implica que deve haver Falses e True s entre os resultados. Portanto, você pode fazer o seguinte:

some = lambda ii: frozenset(bool(i) for i in ii).issuperset((True, False))

# one way to test this is...
test = lambda iterable: (any(iterable) and (not all(iterable))) # see also http://stackoverflow.com/a/16522290/541412

# Some test cases...
assert(some(()) == False)       # all() is true, and any() is false
assert(some((False,)) == False) # any() is false
assert(some((True,)) == False)  # any() and all() are true

assert(some((False,False)) == False)
assert(some((True,True)) == False)
assert(some((True,False)) == True)
assert(some((False,True)) == True)

Uma vantagem desse código é que você só precisa iterar uma vez pelos itens resultantes (booleanos).

Uma desvantagem é que todas essas expressões de verdade são sempre avaliadas e não causam curto-circuito como os operadores or/ and.

Abbafei
fonte
1
Eu acho que isso é uma complicação desnecessária. Por que um frozenset em vez de um conjunto simples e antigo? Por .issupersetque, em vez de apenas verificar o comprimento 2, boolnão é possível retornar nada além de Verdadeiro e Falso. Por que atribuir um lambda (leia-se: função anônima) a um nome em vez de apenas usar um def?
wim 14/05
1
a sintaxe lambda é mais lógica para alguns. de qualquer forma, eles têm o mesmo comprimento, pois você precisa returnse usar def. Eu acho que a generalidade desta solução é boa. não é necessário nos restringir aos booleanos, a questão é essencialmente "como garantir que todos esses elementos ocorram na minha lista". por que setse você não precisa da mutabilidade? mais imutabilidade é sempre melhor se você não precisar do desempenho.
Janus Troelsen
@JanusTroelsen Você está certo no alvo! Essas são algumas razões pelas quais eu fiz dessa maneira; isso torna mais fácil e claro para mim. Eu costumo adaptar o Python à minha maneira de codificar :-).
Abbafei
mas não funcionará em geradores infinitos: P veja minha resposta :) dica: #tee
Janus Troelsen Jan
@JanusTroelsen eu percebo isso :-). Na verdade, eu tinha o contrário (com True / False no conjunto e iterável no parâmetro param) no início, mas percebi que isso não funcionaria com geradores infinitos e que um usuário pode não perceber (já que esse fato não é (ainda) mencionados nos documentos para parâmetros iteráveis ​​de método de conjunto) e, pelo menos assim, é óbvio que ele não precisará de iteradores infinitos. Eu estava ciente, itertools.teemas 1) eu estava procurando uma linha única que fosse simples / pequena o suficiente para garantir a cópia e a colagem, 2) você já deu uma resposta que usa essa técnica :-) #
1144 Abbafei