Comment agissent «et» et «ou» avec des valeurs non booléennes?

99

J'essaie d'apprendre le python et je suis tombé sur du code qui est beau et court mais qui n'a pas totalement de sens

le contexte était:

def fn(*args):
    return len(args) and max(args)-min(args)

Je comprends ce qu'il fait, mais pourquoi python fait-il cela - c'est-à-dire renvoie la valeur plutôt que True / False?

10 and 7-2

renvoie 5. De même, changer le et en ou entraînera une modification de la fonctionnalité. Alors

10 or 7 - 2

J'y retournerais 10.

Est-ce que ce style est légitime / fiable, ou y a-t-il des pièges à ce sujet?

Marcin
la source
1
and(ainsi que or) ne se limite pas à travailler avec ou à renvoyer des valeurs booléennes.
cs95
1
IMNSHO: c'est une façon quelque peu déroutante d'écrire cela; Je ne peux pas dire à la légère s'il est censé renvoyer un booléen (y a-t-il un min et un max distincts) ou un nombre (quelle est la différence entre le min et le max). Dans ce dernier cas, il y a aussi la question de savoir s'il est logique de donner cette différence d'une liste de longueur nulle sous forme de nombre. (Au lieu de Noneou une exception)
ilkkachu
7
Cela fonctionne, comme d'autres personnes l'ont expliqué, mais un problème possible est que s'il revient, 0vous ne pouvez pas dire s'il argsétait vide ou non, mais que tous les éléments étaient égaux.
Surtout Lime
@Esp particularLime: exactement. Je l'ai mentionné dans ma réponse .
Eric Duminil

Réponses:

135

TL; DR

Nous commençons par résumer les deux comportements des deux opérateurs logiques andet or. Ces expressions idiomatiques formeront la base de notre discussion ci-dessous.

and

Renvoie la première valeur Falsy s'il y en a, sinon renvoie la dernière valeur de l'expression.

or

Renvoie la première valeur Truthy s'il y en a, sinon renvoie la dernière valeur de l'expression.

Le comportement est également résumé dans la documentation , en particulier dans ce tableau:

entrez la description de l'image ici

Le seul opérateur renvoyant une valeur booléenne indépendamment de ses opérandes est l' notopérateur.


Évaluations "Vérité" et "Vérité"

La déclaration

len(args) and max(args) - min(args)

Est une manière très pythonique concise (et sans doute moins lisible) de dire "si ce argsn'est pas vide, renvoie le résultat de max(args) - min(args)", sinon retourne 0. En général, il s'agit d'une représentation plus concise d'une if-elseexpression. Par exemple,

exp1 and exp2

Devrait (à peu près) se traduire par:

r1 = exp1
if r1:
    r1 = exp2

Ou équivalent,

r1 = exp1 if exp1 else exp2

De même,

exp1 or exp2

Est équivalent à,

r1 = exp1
if not r1:
    r1 = exp2

exp1et exp2sont des objets Python arbitraires ou des expressions qui renvoient un objet. La clé pour comprendre les utilisations des opérateurs logiques andet orici est de comprendre qu'ils ne se limitent pas à fonctionner ou à renvoyer des valeurs booléennes. Tout objet avec une valeur de vérité peut être testé ici. Cela inclut int, str, list, dict, tuple, set, NoneTypeet objets définis par l' utilisateur. Les règles de court-circuit s'appliquent également.

Mais qu'est-ce que la vérité?
Il fait référence à la manière dont les objets sont évalués lorsqu'ils sont utilisés dans des expressions conditionnelles. @Patrick Haugh résume bien la vérité dans cet article .

Toutes les valeurs sont considérées comme "véridiques", à l'exception des suivantes, qui sont "fausses":

  • None
  • False
  • 0
  • 0.0
  • 0j
  • Decimal(0)
  • Fraction(0, 1)
  • [] - un vide list
  • {} - un vide dict
  • () - un vide tuple
  • '' - un vide str
  • b'' - un vide bytes
  • set() - un vide set
  • un vide range, commerange(0)
  • objets pour lesquels
    • obj.__bool__() Retour False
    • obj.__len__() Retour 0

Une valeur "véridique" satisfera le contrôle effectué par les instructions ifou while. Nous utilisons «véridique» et «faux» pour différencier les boolvaleurs Trueet False.


Comment andfonctionne

Nous nous appuyons sur la question d'OP comme une suite dans une discussion sur la façon dont ces opérateurs dans ces cas.

Étant donné une fonction avec la définition

def foo(*args):
    ...

Comment renvoyer la différence entre la valeur minimale et la valeur maximale dans une liste de zéro ou plusieurs arguments?

Trouver le minimum et le maximum est facile (utilisez les fonctions intégrées!). Le seul hic ici est de gérer correctement le cas du coin où la liste d'arguments pourrait être vide (par exemple, appeler foo()). Nous pouvons faire les deux en une seule ligne grâce à l' andopérateur:

def foo(*args):
     return len(args) and max(args) - min(args)

foo(1, 2, 3, 4, 5)
# 4

foo()
# 0

Depuis andest utilisé, la deuxième expression doit également être évaluée si la première l'est True. Notez que, si la première expression est évaluée comme véridique, la valeur de retour est toujours le résultat de la deuxième expression . Si la première expression est évaluée comme étant Falsy, le résultat renvoyé est le résultat de la première expression.

Dans la fonction ci-dessus, If fooreçoit un ou plusieurs arguments, len(args)est supérieur à 0(un nombre positif), le résultat renvoyé est donc max(args) - min(args). OTOH, si aucun argument n'est passé, len(args)est 0ce qui est Falsy et 0est retourné.

Notez qu'une autre façon d'écrire cette fonction serait:

def foo(*args):
    if not len(args):
        return 0

    return max(args) - min(args)

Ou, plus brièvement,

def foo(*args):
    return 0 if not args else max(args) - min(args)

Si bien sûr, aucune de ces fonctions n'effectue de vérification de type, donc à moins que vous ne fassiez entièrement confiance à l'entrée fournie, ne vous fiez pas à la simplicité de ces constructions.


Comment orfonctionne

J'explique le fonctionnement de orde la même manière avec un exemple artificiel.

Étant donné une fonction avec la définition

def foo(*args):
    ...

Comment feriez-vous foopour retourner tous les numéros 9000?

Nous utilisons orpour gérer le cas d'angle ici. Nous définissons foocomme:

def foo(*args):
     return [x for x in args if x > 9000] or 'No number over 9000!'

foo(9004, 1, 2, 500)
# [9004]

foo(1, 2, 3, 4)
# 'No number over 9000!'

fooeffectue une filtration sur la liste pour conserver tous les numéros 9000. S'il existe de tels nombres, le résultat de la compréhension de la liste est une liste non vide qui est Vérité, donc elle est retournée (court-circuit en action ici). S'il n'existe pas de tels nombres, alors le résultat de la liste comp est []qui est Falsy. Ainsi, la deuxième expression est maintenant évaluée (une chaîne non vide) et est renvoyée.

En utilisant des conditions, nous pourrions réécrire cette fonction comme suit:

def foo(*args):
    r = [x for x in args if x > 9000]
    if not r:
        return 'No number over 9000!' 

    return r

Comme précédemment, cette structure est plus flexible en termes de gestion des erreurs.

cs95
la source
33
Il n'est pas «pythonique» de sacrifier toute clarté à la brièveté, ce qui, je pense, est le cas ici. Ce n'est pas une construction simple.
DBedrenko
11
Je pense qu'il faut noter que les expressions conditionnelles Python ont rendu cette syntaxe moins courante. Je préfère certainement max (args) - min (args) si len (args) sinon 0 à l'original.
richardb
3
Une autre chose courante qui prête à confusion au début, consiste à attribuer une valeur s'il n'y en a pas: "some_var = arg or 3"
Erik
12
@Baldrickk avant que les gens ne commencent à dénigrer cette syntaxe en faveur des opérateurs ternaires, gardez à l'esprit que lorsqu'il s'agit d'expressions de conditions n-aires, les opérateurs ternaires peuvent rapidement devenir incontrôlables. Par exemple, if ... else (if ... else (if ... else (if ... else ...)))peut tout aussi bien être réécrit que ... and ... and ... and ... and ...et à ce stade, il devient vraiment difficile d'argumenter la lisibilité dans les deux cas.
cs95
4
Ce n'est pas pythonique de sacrifier la clarté pour la brièveté, mais ce n'est pas le cas. C'est un idiome bien connu. C'est un idiome que vous devez apprendre, comme n'importe quel autre idiome, mais ce n'est guère «sacrifier la clarté».
Miles Rout
18

Citations à partir de documents Python

Notez que ni andni ne or restreignent la valeur et le type auxquels ils retournent Falseet True, mais renvoient plutôt le dernier argument évalué . Ceci est parfois utile, par exemple, si sest une chaîne qui doit être remplacée par une valeur par défaut si elle est vide, l'expression s or 'foo'donne la valeur souhaitée.

C'est ainsi que Python a été conçu pour évaluer les expressions booléennes et la documentation ci-dessus nous donne un aperçu de la raison pour laquelle ils l'ont fait.

Pour obtenir une valeur booléenne, transformez-la simplement.

return bool(len(args) and max(args)-min(args))

Pourquoi?

Court-circuit.

Par exemple:

2 and 3 # Returns 3 because 2 is Truthy so it has to check 3 too
0 and 3 # Returns 0 because 0 is Falsey and there's no need to check 3 at all

Il en va de ormême, c'est-à-dire qu'il renverra l'expression qui est Vérité dès qu'il la trouve, car l'évaluation du reste de l'expression est redondante.

Au lieu de retourner hardcore Trueou False, Python retourne Truthy ou Falsey , qui vont de toute façon évaluer Trueou False. Vous pouvez utiliser l'expression telle quelle, et cela fonctionnera toujours.


Pour savoir ce qui est Truthy et Falsey , consultez la réponse de Patrick Haugh

Amit Joki
la source
7

et et ou exécutent une logique booléenne, mais ils renvoient l'une des valeurs réelles lors de la comparaison. Lors de l'utilisation de et , les valeurs sont évaluées dans un contexte booléen de gauche à droite. 0, '', [], (), {} et None sont faux dans un contexte booléen; tout le reste est vrai.

Si toutes les valeurs sont vraies dans un contexte booléen, et renvoie la dernière valeur.

>>> 2 and 5
5
>>> 2 and 5 and 10
10

Si une valeur est fausse dans un contexte booléen et renvoie la première valeur fausse.

>>> '' and 5
''
>>> 2 and 0 and 5
0

Donc le code

return len(args) and max(args)-min(args)

renvoie la valeur de max(args)-min(args)quand il y a des arguments sinon elle retourne len(args)qui est 0.

Nithin Varghese
la source
5

Est-ce que ce style est légitime / fiable, ou y a-t-il des pièges à ce sujet?

C'est légitime, c'est une évaluation de court-circuit où la dernière valeur est retournée.

Vous donnez un bon exemple. La fonction retournera 0si aucun argument n'est passé, et le code n'a pas à vérifier un cas particulier sans argument passé.

Une autre façon d'utiliser cela, est de définir par défaut les arguments None sur une primitive mutable, comme une liste vide:

def fn(alist=None):
    alist = alist or []
    ....

Si une valeur non véridique lui est transmise par alistdéfaut, une liste vide, moyen pratique d'éviter une ifinstruction et le piège de l'argument mutable par défaut

salade
la source
3

Gotchas

Oui, il y a quelques pièges.

fn() == fn(3) == fn(4, 4)

Premièrement, si fnretourne 0, vous ne pouvez pas savoir s'il a été appelé sans aucun paramètre, avec un paramètre ou avec plusieurs paramètres égaux:

>>> fn()
0
>>> fn(3)
0
>>> fn(3, 3, 3)
0

Que veut fndire?

Ensuite, Python est un langage dynamique. Il n'est spécifié nulle part ce que fnfait, ce que devrait être son entrée et à quoi devrait ressembler sa sortie. Par conséquent, il est vraiment important de nommer la fonction correctement. De même, les arguments n'ont pas besoin d'être appelés args. delta(*numbers)ou calculate_range(*numbers)pourrait mieux décrire ce que la fonction est censée faire.

Erreurs d'argument

Enfin, l' andopérateur logique est censé empêcher la fonction d'échouer si elle est appelée sans aucun argument. Cela échoue toujours si un argument n'est pas un nombre, cependant:

>>> fn('1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'
>>> fn(1, '2')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: '>' not supported between instances of 'str' and 'int'
>>> fn('a', 'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'

Alternative possible

Voici une façon d'écrire la fonction selon le "Plus facile de demander pardon que la permission". principe :

def delta(*numbers):
    try:
        return max(numbers) - min(numbers)
    except TypeError:
        raise ValueError("delta should only be called with numerical arguments") from None
    except ValueError:
        raise ValueError("delta should be called with at least one numerical argument") from None

Par exemple:

>>> delta()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in delta
ValueError: delta should be called with at least one numerical argument
>>> delta(3)
0
>>> delta('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta(3, 4.5)
1.5
>>> delta(3, 5, 7, 2)
5

Si vous ne voulez vraiment pas lever d'exception lors de l' deltaappel sans aucun argument, vous pouvez retourner une valeur qui ne serait pas possible autrement (par exemple -1ou None):

>>> def delta(*numbers):
...     try:
...         return max(numbers) - min(numbers)
...     except TypeError:
...         raise ValueError("delta should only be called with numerical arguments") from None
...     except ValueError:
...         return -1 # or None
... 
>>> 
>>> delta()
-1
Eric Duminil
la source
0

Est-ce que ce style est légitime / fiable, ou y a-t-il des pièges à ce sujet?

Je voudrais ajouter à cette question qu'il est non seulement légitime et fiable, mais aussi ultra pratique. Voici un exemple simple:

>>>example_list = []
>>>print example_list or 'empty list'
empty list

Par conséquent, vous pouvez vraiment l'utiliser à votre avantage. Pour être conscient, voici comment je le vois:

Or opérateur

L' oropérateur de Python renvoie la première valeur Truth-y, ou la dernière valeur, et s'arrête

And opérateur

L' andopérateur de Python renvoie la première valeur False-y, ou la dernière valeur, et s'arrête

Dans les coulisses

En python, tous les nombres sont interprétés comme Truesauf 0. Par conséquent, en disant:

0 and 10 

est le même que:

False and True

Ce qui est clairement False. Il est donc logique qu'il renvoie 0

scharette
la source
0

Oui. C'est le comportement correct et la comparaison.

Au moins dans le python, les A and Brendements Bse Asont essentiellement Truecompris si Aest non nulle, NE NonePAS un récipient vide (tel qu'un vide list, dictetc.). Aest renvoyé IFF Aest essentiellement Falseou Noneou Empty ou Null.

D'autre part, les A or Brendements Ase Asont essentiellement Truecompris si Aest non nul, NE NonePAS un récipient vide (comme un vide list, dictetc.), sinon il retourne B.

Il est facile de ne pas remarquer (ou d'oublier) ce comportement car, en Python, tout non-nullobjet non vide évalué à True est traité comme un booléen.

Par exemple, tout ce qui suit affichera "True"

if [102]: 
    print "True"
else: 
    print "False"

if "anything that is not empty or None": 
    print "True"
else: 
    print "False"

if {1, 2, 3}: 
    print "True"
else: 
    print "False"

Par contre, tout ce qui suit affichera "Faux"

if []: 
    print "True"
else: 
    print "False"

if "": 
    print "True"
else: 
    print "False"

if set ([]): 
    print "True"
else: 
    print "False"
emmanuelsa
la source
Je vous remercie. Je voulais écrire Ac'est essentiellement True. Corrigée.
emmanuelsa
0

pour comprendre de manière simple,

ET : if first_val is False return first_val else second_value

par exemple:

1 and 2 # here it will return 2 because 1 is not False

mais,

0 and 2 # will return 0 because first value is 0 i.e False

et => si quelqu'un est faux, ce sera faux. si les deux sont vrais alors seulement cela deviendra vrai

OU : if first_val is False return second_val else first_value

la raison est que si first est faux, il vérifie si 2 est vrai ou non.

par exemple:

1 or 2 # here it will return 1 because 1 is not False

mais,

0 or 2 # will return 2 because first value is 0 i.e False

ou => si quelqu'un est faux, ce sera vrai. donc si la première valeur est fausse, quelle que soit la valeur 2 supposée. il renvoie donc une seconde valeur quelle qu'elle soit.

si quelqu'un est vrai, cela deviendra vrai. si les deux sont faux, cela deviendra faux.

Mohideen bin Mohammed
la source