Python-Syntax für "wenn a oder b oder c, aber nicht alle"

130

Ich habe ein Python-Skript, das entweder null oder drei Befehlszeilenargumente empfangen kann. (Entweder wird das Standardverhalten ausgeführt oder es müssen alle drei angegebenen Werte angegeben werden.)

Was ist die ideale Syntax für so etwas wie:

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
quelle
4
Beginnen Sie vielleicht mit etwas wie `if len (sys.argv) == 0:
Edgar Aroutiounian
6
@EdgarAroutiounian len(sys.argv)wird immer mindestens 1 sein: Es enthält die ausführbare Datei als argv[0].
RoadieRich
10
Der Hauptteil der Frage stimmt nicht mit dem Titel der Frage überein. Möchten Sie überprüfen, ob a oder b oder c, aber nicht alle "oder" ob genau eines von a, b und c "(wie der von Ihnen angegebene Ausdruck)?
Doug McClean
2
Was können Sie über a + b + c sagen?
Gukoff
6
Warten Sie, Frage, es kann entweder null oder drei Argumente annehmen. if not (a and b and c)Könntest du nicht einfach sagen (null Argumente) und dann if a and b and c(alle drei Argumente)?
Akolyth

Antworten:

236

Wenn Sie eine minimale Form meinen, gehen Sie wie folgt vor:

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

Welches übersetzt den Titel Ihrer Frage.

UPDATE: Wie von Volatility und Supr richtig gesagt, können Sie das Gesetz von De Morgan anwenden und ein Äquivalent erhalten:

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

Mein Rat ist, die Form zu verwenden, die für Sie und andere Programmierer wichtiger ist. Das erste bedeutet "es gibt etwas Falsches, aber auch etwas Wahres" , das zweite "Es gibt etwas Wahres, aber nicht alles" . Wenn ich dies in der Hardware optimieren oder tun würde, würde ich die zweite wählen, hier nur die am besten lesbare auswählen (auch unter Berücksichtigung der Bedingungen, die Sie testen werden, und ihrer Namen). Ich habe den ersten ausgewählt.

Stefano Sanfilippo
quelle
3
Alles gute Antworten, aber dies gewinnt an Prägnanz mit großem Kurzschluss. Vielen Dank an alle!
Chris Wilson
38
Ich würde es noch prägnanter machen und mitif not (a and b and c) and (a or b or c)
Volatility
208
Oder sogar if (a or b or c) and not (a and b and c), um perfekt zum Titel zu passen;)
Supr
3
@HennyH Ich glaube, die Frage fragt nach "mindestens eine Bedingung wahr, aber nicht alle", nicht "nur eine Bedingung wahr".
Volatilität
63
@Suprif any([a,b,c]) and not all([a,b,c])
ewigmatt
238

Wie wäre es mit:

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

Andere Variante:

if 1 <= sum(map(bool, conditions)) <= 2:
   ...
defuz
quelle
2
sum(conditions)kann schief gehen, wenn einer von ihnen 2zum Beispiel zurückkehrt, was ist True.
Eumiro
7
Wahre müßten Sie einen hässlichensum(map(bool, conditions))
jamylak
5
Beachten Sie, dass dies kein Kurzschluss ist, da alle Bedingungen vorab ausgewertet werden.
Georg
14
@PaulScheltema Das erste Formular ist für jeden verständlicher.
cmh
6
Dieses "alles und nicht alles" ist die beste und klarste der booleschen Methoden. Denken Sie nur an die wichtige Unterscheidung zwischen einem vorhandenen Argument und einem "wahrheitsgemäßen"
Argument
115

Diese Frage hatte bereits viele hoch bewertete Antworten und eine akzeptierte Antwort, aber alle waren bisher durch verschiedene Ausdrucksmöglichkeiten des booleschen Problems abgelenkt und haben einen entscheidenden Punkt übersehen:

Ich habe ein Python-Skript, das entweder null oder drei Befehlszeilenargumente empfangen kann. (Entweder wird das Standardverhalten ausgeführt oder es müssen alle drei angegebenen Werte angegeben werden.)

Diese Logik sollte nicht in erster Linie in der Verantwortung Ihres Codes liegen , sondern vomargparseModulbehandelt werden. Schreiben Sie keine komplexe if-Anweisung, sondern richten Sie Ihren Argument-Parser lieber wie folgt ein:

#!/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)

Und ja, es sollte eine Option sein, kein Positionsargument, denn es ist schließlich optional .


bearbeitet: Um das Problem von LarsH in den Kommentaren anzusprechen, finden Sie unten ein Beispiel dafür, wie Sie es schreiben könnten, wenn Sie sicher wären, dass Sie die Schnittstelle mit 3 oder 0 Positionsargumenten haben möchten. Ich bin der Meinung, dass die vorherige Benutzeroberfläche einen besseren Stil hat, da optionale Argumente Optionen sein sollten , aber der Vollständigkeit halber hier ein alternativer Ansatz. Beachten Sie das überschreibende kwargusagebeim Erstellen Ihres Parsers, daargparsesonst automatisch eine irreführende Verwendungsnachricht generiert wird!

#!/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)

Hier einige Anwendungsbeispiele:

# 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
quelle
4
Ja, das habe ich absichtlich hinzugefügt. Es wäre möglich, das Argument positionell zu machen und zu erzwingen, dass genau 3 oder 0 verbraucht werden, aber es würde keine gute CLI ergeben, daher habe ich es nicht empfohlen.
wim
8
Separate Ausgabe. Sie glauben nicht, dass es eine gute CLI ist, und Sie können für diesen Punkt argumentieren, und das OP kann überzeugt werden. Ihre Antwort weicht jedoch erheblich genug von der Frage ab, sodass die Änderung der Spezifikation erwähnt werden muss. Sie scheinen die Spezifikation so zu biegen, dass sie zum verfügbaren Werkzeug passt, ohne die Änderung zu erwähnen.
LarsH
2
@LarsH OK, ich habe ein Beispiel hinzugefügt, das besser zu der in der Frage implizierten ursprünglichen Schnittstelle passt. Jetzt wird das Werkzeug
gebogen
2
Dies ist die einzige Antwort, die ich positiv bewertet habe. +1 für die Beantwortung der eigentlichen Frage .
Jonathon Reinhart
1
+1. Die Form der CLI ist ein wichtiges tangentiales Thema, das nicht vollständig getrennt ist, wie eine andere Person sagte. Ich habe Ihren Beitrag ebenso wie andere positiv bewertet - Ihr Beitrag geht dem Problem auf den Grund und bietet eine elegante Lösung, während andere Beiträge die wörtliche Frage beantworten. Und beide Arten von Antworten sind nützlich und verdienen +1.
Ben Lee
32

Ich würde gehen für:

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

Ich denke, dies sollte ziemlich effizient kurzschließen

Erläuterung

Wenn Sie condseinen Iterator erstellen, wird die erste Verwendung von anykurzgeschlossen und der Iterator zeigt auf das nächste Element, wenn ein Element wahr ist. Andernfalls wird die gesamte Liste verbraucht und sein False. Der nächste anynimmt die verbleibenden Elemente in der Iterable und stellt sicher, dass es keine anderen wahren Werte gibt ... Wenn ja, kann die gesamte Aussage nicht wahr sein, daher gibt es kein einziges Element (also Kurzschlüsse) nochmal). Der letzte anywird entweder zurückkehren Falseoder das iterable erschöpfen und sein True.

Hinweis: Die obige Prüfung prüft, ob nur eine einzige Bedingung festgelegt ist


Wenn Sie überprüfen möchten, ob ein oder mehrere Elemente, aber nicht jedes Element festgelegt ist, können Sie Folgendes verwenden:

not all(conds) and any(conds)
Jon Clements
quelle
5
Ich verstehe es nicht Es lautet wie folgt: Wenn wahr und nicht wahr. Hilf mir, zu verstehen.
rGil
1
@rGil: Es lautet wie "wenn einige Äpfel rot sind und andere nicht" - es ist dasselbe wie zu sagen "einige Äpfel sind rot, aber nicht alle".
Georg
2
Selbst mit einer Erklärung kann ich das Verhalten nicht verstehen ... Mit [a, b, c] = [True, True, False]sollte Ihr Code nicht "gedruckt" werden False, während die erwartete Ausgabe erfolgt True?
Awesoon
6
Das ist ziemlich klug, ABER: Ich würde diesen Ansatz verwenden, wenn Sie nicht wüssten, wie viele Bedingungen Sie im Voraus hatten, aber für eine feste, bekannte Liste von Bedingungen lohnt sich der Verlust der Lesbarkeit einfach nicht.
flauschig
4
Dies schließt nicht kurz. Die Liste wird vollständig erstellt, bevor sie an übergeben wird iter. anyund allwird die Liste träge konsumieren, stimmt, aber die Liste wurde bereits vollständig ausgewertet, als Sie dort ankommen!
icktoofay
22

Der englische Satz:

"Wenn a oder b oder c, aber nicht alle"

Übersetzt in diese Logik:

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

Das Wort "aber" impliziert normalerweise eine Konjunktion, mit anderen Worten "und". Darüber hinaus bedeutet "alle von ihnen" eine Verbindung von Bedingungen: diese Bedingung und diese Bedingung und andere Bedingung. Das "nicht" invertiert diese gesamte Konjunktion.

Ich bin nicht damit einverstanden, dass die Antwort akzeptiert. Der Autor hat es versäumt, die einfachste Interpretation auf die Spezifikation anzuwenden, und es versäumt, das De-Morgan-Gesetz anzuwenden, um den Ausdruck auf weniger Operatoren zu vereinfachen:

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

während behauptet wird, dass die Antwort eine "minimale Form" ist.

Kaz
quelle
Eigentlich ist diese Form minimal. Es ist die minimale PoS-Form für den Ausdruck.
Stefano Sanfilippo
10

Dies wird zurückgegeben, Truewenn eine und nur eine der drei Bedingungen erfüllt ist True. Wahrscheinlich das, was Sie in Ihrem Beispielcode wollten.

if sum(1 for x in (a,b,c) if x) == 1:
Eumiro
quelle
Nicht so hübsch wie die Antwort von @defuz
jamylak
10

Was ist mit: (einzigartiger Zustand)

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

Beachten Sie, wenn Sie auch zwei Bedingungen zulassen, können Sie dies tun

if (bool(a) + bool(b) + bool(c) in [1,2]):
Casimir et Hippolyte
quelle
1
Für die Aufzeichnung fragt die Frage nach zwei Bedingungen. Mindestens einer, aber nicht alle = 1 von allen oder 2 von allen
Marius Balčytis
IMHO sollten Sie den zweiten als buchstabieren 1 <= bool(a) + bool(b) + bool(c) <= 2.
Stellen Sie Monica
6

Um klar zu sein, möchten Sie Ihre Entscheidung basierend darauf treffen, wie viele der Parameter logisch WAHR sind (im Fall von Zeichenfolgenargumenten - nicht leer)?

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

Dann haben Sie eine Entscheidung getroffen:

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

Jetzt ist die Logik klarer.

Donau-Seemann
quelle
5

Und warum nicht einfach zählen?

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
quelle
5

Wenn es Ihnen nichts ausmacht, ein bisschen kryptisch zu sein, können Sie einfach rollen, mit 0 < (a + b + c) < 3dem Sie zurückkehrentrue wenn Sie zwischen einer und zwei wahre Aussagen haben, und falsch, wenn alle falsch sind oder keine falsch ist.

Dies vereinfacht sich auch, wenn Sie Funktionen zum Auswerten der Bools verwenden, da Sie die Variablen nur einmal auswerten. Dies bedeutet, dass Sie die Funktionen inline schreiben können und die Variablen nicht vorübergehend speichern müssen. (Beispiel : 0 < ( a(x) + b(x) + c(x) ) < 3.)

Spinno
quelle
4

Die Frage besagt, dass Sie entweder alle drei Argumente (a und b und c) oder keines von ihnen (nicht (a oder b oder c)) benötigen.

Das gibt:

(a und b und c) oder nicht (a oder b oder c)

Entspannen in Zypern
quelle
4

Soweit ich weiß, haben Sie eine Funktion, die 3 Argumente empfängt. Wenn dies nicht der Fall ist, wird sie mit dem Standardverhalten ausgeführt. Da Sie nicht erklärt haben, was passieren soll, wenn 1 oder 2 Argumente angegeben werden, gehe ich davon aus, dass es einfach das Standardverhalten ausführen sollte. In diesem Fall finde ich die folgende Antwort sehr vorteilhaft:

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

Wenn Sie jedoch möchten, dass 1 oder 2 Argumente unterschiedlich behandelt werden:

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?)

Hinweis: Dies setzt voraus, dass " False" Werte nicht an diese Methode übergeben werden.

Inbar Rose
quelle
Die Überprüfung des Wahrheitswertes eines Arguments unterscheidet sich von der Überprüfung, ob ein Argument vorhanden ist oder nicht
wim
@wim Konvertiert also eine Frage, die Ihrer Antwort entspricht. Das Argparse-Modul hat nichts mit der Frage zu tun, es fügt einen weiteren Import hinzu, und wenn das OP überhaupt nicht plant, Argparse zu verwenden, hilft es ihnen überhaupt nicht. Wenn das "Skript" nicht eigenständig ist, sondern ein Modul oder eine Funktion in einem größeren Satz von Code, verfügt es möglicherweise bereits über einen Argumentparser, und diese bestimmte Funktion in diesem größeren Skript kann standardmäßig oder angepasst werden. Aufgrund der begrenzten Informationen aus dem OP kann ich nicht wissen, wie die Methode funktionieren soll, aber es ist sicher anzunehmen, dass das OP keine Bools passiert.
Inbar Rose
Die Frage lautete ausdrücklich "Ich habe ein Python-Skript, das entweder null oder drei Befehlszeilenargumente empfangen kann", nicht "Ich habe eine Funktion, die drei Argumente empfängt". Da das argparse-Modul die bevorzugte Methode zum Behandeln von Befehlszeilenargumenten in Python ist, hat es automatisch alles mit der Frage zu tun. Schließlich ist Python "Batterien enthalten" - es gibt keinen Nachteil beim "Hinzufügen eines weiteren Imports", wenn dieses Modul Teil der Standardbibliotheken ist.
wim
@wim Die Frage ist ziemlich unklar (Körper passt zum Beispiel nicht zum Titel). Ich denke, die Frage ist unklar genug, dass dies eine gültige Antwort für eine Interpretation ist.
Stellen Sie Monica
2

Wenn Sie mit einem Iterator von Bedingungen arbeiten, kann der Zugriff langsam sein. Sie müssen jedoch nicht mehr als einmal auf jedes Element zugreifen, und Sie müssen nicht immer alles lesen. Hier ist eine Lösung, die mit unendlichen Generatoren funktioniert:

#!/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
quelle
0

Wenn jedes gegeben boolist Trueoder wenn jedes gegeben boolist False...
sie alle gleich!

Wir müssen also nur zwei Elemente finden, die unterschiedliche boolWerte ergeben, um
zu wissen, dass es mindestens eines Trueund mindestens eines gibt False.

Meine kurze Lösung:

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

Ich glaube es schließt kurz, weil AFAIK a==b==cgleich ista==b and b==c .

Meine verallgemeinerte Lösung:

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)

Ich habe auch Code geschrieben, der sich mit mehreren Iterablen befasst, aber ich habe ihn von hier gelöscht, weil ich denke, dass er sinnlos ist. Es ist jedoch noch hier verfügbar .

GingerPlusPlus
quelle
-2

Dies ist im Grunde eine "einige (aber nicht alle)" Funktionalität (im Gegensatz zu any()undall() eingebauten Funktionen).

Dies impliziert, dass es Falses und True s unter den Ergebnissen geben sollte. Daher können Sie Folgendes tun:

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)

Ein Vorteil dieses Codes besteht darin, dass Sie die resultierenden (booleschen) Elemente nur einmal durchlaufen müssen.

Ein Nachteil ist, dass alle diese Wahrheitsausdrücke immer ausgewertet werden und nicht wie die / -Operatoren kurzschließen .orand

Abbafei
quelle
1
Ich denke, das ist unnötige Komplikation. Warum ein Frozenset anstelle eines einfachen alten Sets? Warum, .issupersetanstatt nur nach Länge 2 zu suchen, boolkann ich sowieso nichts anderes als Wahr und Falsch zurückgeben. Warum einem Namen ein Lambda (sprich: anonyme Funktion) zuweisen, anstatt nur ein Def zu verwenden?
wim
1
Die Lambda-Syntax ist für manche logischer. Sie sind sowieso gleich lang, da Sie sie benötigen, returnwenn Sie sie verwenden def. Ich finde die Allgemeinheit dieser Lösung gut. Es ist nicht notwendig, uns auf Boolesche Werte zu beschränken. Die Frage lautet im Wesentlichen: "Wie stelle ich sicher, dass alle diese Elemente in meiner Liste vorkommen?" Warum, setwenn Sie die Veränderlichkeit nicht brauchen? Mehr Unveränderlichkeit ist immer besser, wenn Sie die Leistung nicht benötigen.
Janus Troelsen
@ JanusTroelsen Du bist genau richtig! Dies sind einige Gründe, warum ich es so gemacht habe; es macht es mir einfacher und klarer. Ich neige dazu, Python an meine Codierungsweise anzupassen :-).
Abbafei
aber es wird nicht auf unendlichen Generatoren funktionieren: P siehe meine Antwort :) Hinweis:tee
Janus Troelsen
@ JanusTroelsen Mir ist klar :-). Eigentlich hatte ich es zuerst umgekehrt (mit True / False in der Menge und iterable in der Methode param), aber mir wurde klar, dass dies mit unendlichen Generatoren nicht funktionieren würde, und ein Benutzer könnte es nicht erkennen (da dies nicht der Fall ist) (noch) in den Dokumenten für iterable set method params erwähnt), und zumindest so ist es offensichtlich, dass es nicht unendlich viele Iteratoren braucht. Ich war mir bewusst, itertools.teeaber 1) ich suchte nach einem Einzeiler, der einfach / klein genug war, um das Einfügen von Kopien zu rechtfertigen, 2) Sie gaben bereits eine Antwort, die diese Technik verwendet :-)
Abbafei