Dans beaucoup de langages populaires, and
et or
sont écrits &&
et ||
. Ces symboles existent en Python, mais ils sont là pour appliquer des opérations binaires :
>>> bin(0b010 & 0b111) '0b10' >>> bin(0b010 | 0b111) '0b111' |
Ce n’est néanmoins pas la seule bizarrerie de Python dans le domaine.
Shortcuts
Les opérateurs and
et or
court-circuitent les conditions dès que possible, c’est à dire qu’ils retournent la valeur au plus tôt, même si ça signifie ne pas exécuter tout le code.
Par exemple, prenons deux fonctions:
def vrai(): print('Yeah !') return True def faux(): print('Errrr...') return False |
Si je fais un or
dessus, ça va me retourner True
, et afficher deux messages :
>>> faux() or vrai() Errrr... Yeah ! True |
Mais si j’INVERSE les deux fonctions, alors je n’aurais qu’un seul message qui va s’afficher :
>>> vrai() or faux() Yeah ! True |
La raison est que or
sait qu’il peut retourner True
dès qu’il obtient au moins une valeur True
. vrai()
retourne True
, donc or
sait que tout la condition sera forcément vraie, et il n’exécute pas le code du reste de la condition. Ainsi, faux()
n’est jamais appelée.
and
fait pareil :
>>> vrai() and faux() Yeah ! Errrr... False |
Et à l’envers :
>>> faux() and vrai() Errrr... False |
Car dans le second cas, and
sait qu’il doit avoir toutes les valeurs à True
pour renvoyer True
. Comme il reçoit False
dès le premier test, il ne va pas plus loin, et vrai()
n’est jamais appelée.
Le but de cette fonctionnalité est d’autoriser le développeur à mettre les fonctions qui sont les plus gourmandes en ressource tout à droite de la condition, ainsi elle ne seront pas toujours appelées, ce qui améliore les perfs.
Si vous avez besoin que les fonctions soient toujours appelées car elles ont des effets de bord (c’est mal, boooouh !), il suffit de mettre leurs résultats dans des variables :
>>> a = vrai() Yeah ! >>> b = faux() Errrr... >>> b and a False |
Pas de bool
La plupart des opérateurs utilisés pour faire des tests retournent des booléans :
>>> 1 > 2 False >>> "a" in "chat" True |
Mais and
et or
ne retournent pas des booléans. Dès qu’ils sont certains du résultats de la condition, ils retournent la valeurs qu’ils ont sous la main.
Cela est du au fait qu’en Python, tout a une valeur True
ou False
dans un contexte booléen. Pour faire simple, n’importe quel objet mis dans une condition vaut soit True
, soit False
.
Par exemple, une liste vide vaut False
dans une condition, une liste non vide vaut True
:
>>> couleurs = [] >>> if couleurs: ...: print("J'ai une couleur !") >>> couleurs.append('rouge') >>> if couleurs: print("J'ai une couleur !") J'ai une couleur ! |
On peut le vérifier facilement :
>>> bool([]) False >>> bool(['rouge']) True |
Il est facile de se souvenir de ce qui est faux ou vrai en Python. False
, None
, 0
et tout ce qui est vide est faux :
>>> for x in (False, None, 0, "", [], set(), {}, ()): ...: print(type(x), bool(x)) ...: <class 'bool'>, False <class 'NoneType'>, False <class 'int'>, False <class 'str'>, False <class 'list'>, False <class 'set'>, False <class 'dict'>, False <class 'tuple'>, False |
Tout le reste est vrai :
>>> for x in (Ellipsis, True, 432, "foo", ["bar"], set("ba"), {"pa": "pa"}, ("doh",), lambda : None, len): print(type(x), bool(x)) <class 'ellipsis'> True <class 'bool'> True <class 'int'> True <class 'str'> True <class 'list'> True <class 'set'> True <class 'dict'> True <class 'tuple'> True <class 'function'> True <class 'builtin_function_or_method'> True |
Du coup, and
et or
vont vérifier la valeur de chaque objet de la condition, et retourner le premier à partir duquel ils sont certains du résultat de la condition entière.
Par exemple, si je fais :
>>> True and True and False and False False |
and
n’est certain que la condition est fausse qu’au moment où on attend le premier False
. C’est donc ce False
qu’il retourne.
Cela est beaucoup plus clair quand on le fait avec des objets plus complexes :
>>> "a" and 1 and [] and {} [] |
Puisque :
>>> bool('a') True >>> bool(1) True >>> bool([]) False >>> bool({}) False |
and
n’est certain du résultat de la condition qu’en arrivant sur []
, qu’il retourne.
Si tous les éléments sont vrais, il va donc prendre le dernier :
>>> "a" and 1 and True and [1, 2, 3] [1, 2, 3] |
C’est la même chose pour or
:
>>> "" or None or False or 0 0 |
Là, or
ne peut pas savoir si la condition est fausse avant d’arriver au tout dernier élément, qu’il retourne.
Mais si je glisse un truc vrai dans le lot :
>>> "" or {1: 2} or False or 0 {1: 2} |
Comme il n’a besoin que d’un élément vrai pour que toute la condition soit vraie, dès qu’il en rencontre un, il le retourne.
Il n’y a pas de XOR
Le “ou” exclusif, opération qui retourne vrai seulement si un élément est vrai mais pas l’autre, n’existe pas sous la forme d’un opérateur en Python. Évidement on peut l’émuler manuellement :
def xor(a, b): return (a and not b) or (not a and b) |
Mais une astuce de sioux permet un résultat plus court avec une syntaxe un poil plus proche des langages qui possèdent cet opérateur :
bool(a) ^ bool(b) |
Exemple :
>>> bool(['pomme']) ^ bool([]) True >>> bool(['pomme']) ^ bool(['banane']) False |
^ est en effet l’opérateur XOR pour les opérations binaires. La partie marrante, c’est qu’en Python :
>>> True == 1 True >>> False == 0 True |
Et comme :
>>> 1 ^ 1 0 >>> 1 ^ 0 1 |
Alors:
>>> True ^ True False >>> True ^ False True |
On obtient le résultat voulu.
Oui, c’est un peu tordu, je vous l’accorde.
Merci pour cet article ; moi qui joue beaucoup avec les conditions sur des tests assez longs et “bizarres”, je comprends certains comportements justement… On ne se rend pas compte de la granularité de ce genre d’opérateur (et du coup de celle des variables).
Bonjour
Très bon article, comme toujours.
Ceci dit, la majorité des langages font de même. Par exemple, en C, on peut très bien écrire
if (pt && pt == truc)
où *pt ne sera évalué que si pt est non nul. C’est fait non seulement pour (en effet) pouvoir mettre les fonctions les plus gourmandes à droite ; mais aussi (et je pense “surtout”) pour éviter les bugs lors de l’évaluation. En effet, que se passerait-il sipt
était évalué même si la première condition n’est pas vérifiée et que “pt” vallait NULL ???Le seul langage qui (à ma connaissance) ne se comporte pas ainsi est le Bourne shell (et ses dérivés)
#!/bin/bash
fct() {
echo "i'm fct" >&2
echo "true"
}
test -z "eee" -a -n "$(fct)" && echo "ok" # Affichera "i'm fct" alors que le test -z "eee" étant faux, l'expression est définitivement fausse
test -n "eee" -o -n "$(fct)" && echo "ok" # Affichera là encore "i'm fct" alors que le test -n "eee" étant vrai, l'expression est définitivement vraie (et affichera aussi "ok")
Non, Fred, sauf (grosse) erreur de ma part en C/C++, ces opérations (&&, ||) sont binaires et évaluent toute l’expression du test.
Visual basic ne short circuite pas non plus : http://stackoverflow.com/questions/486722/why-is-short-circuiting-not-the-default-behavior-in-vb. La plupart des langages modernes le font maintenant.
J’ai dis des bêtises, c’est d’actualité maintenant.
@Fred le
&&
du bash fait bien de l’évaluation paresseuse@Fred bash fait la même chose sur ces conditions.
Seulement, test, c’est une builtin, donc une commande, donc ça intervient après l’évaluation des paramètres.
C’est comme si on disait qu’en Python ça ne le faisait pas car
def test(a, b): a and b
test(print(1), print(2))
Y a-t-il une raison pour laquelle il n’existe pas d’opérateur
xor
built-in en Python ?L’équipe de dev de Python a pour habitude de faire très attention au nombres de builtins disponibles pour éviter de saturer l’espace de nom racine. De fait, xor n’étant pas souvent utilisé, et pouvant être facilement émulé avec des opérateurs classiques, il n’a pas rempli les critères pour être ajouté.
Hello,
Article très intéressant. Une petite erreur s’est glissé cela dit (il me semble) :
faux() or vrai()
Yeah !
Errrr...
True
devrait être
faux() or vrai()
Errrr...
Yeah !
True
Bien vu.
@Xavier Combelle
Le “&&” n’est pas un opérateur d’évaluation d’expression mais un opérateur de séquence d’instructions. Si l’instruction placée à gauche est vraie alors il continue sur l’instruction de droite (je m’en sers d’ailleurs dans mon exemple). Bien évidemment, si l’instruction de gauche est fausse alors l’instruction de droite n’est pas exécutée ce qui donne un résultat similaire, effectivement, à une évaluation (tu la nommes “paresseuse”, moi je la nomme “optimisée”) ; mais moi je parlais du comportement de l’évaluateur de base “test” (qui est d’ailleurs identique au programme “/usr/bin/test”)
@entwanne
J’aime bien ton analogie. Effectivement je n’avais jamais vu les choses sous cet angle ;)
Y’a une alternative à ^ pour le XOR: bool(a) != bool(b). Suivant les cas et la manière de penser des gens, ça peut être plus clair. Perso je l’utilise parce que l’opérateur ^ est très peu courant en python
Autre utilisation pratique du fait que les opérateurs retournent le dernier objet évalué, c’est pour l’initialisation par défaut :
mon_repas = get_repas() or ['brioche', 'pastis']
Si get_repas renvoie une valeur évaluée à False (None ou une liste vide, par exemple), alors mon_repas prendra pour valeur [‘brioche’, ‘pastis’]
@Sam:
c’est que les qu’en Python
==>c’est qu’en Python
non ?Super intéressant.
En effet :)
Une autre raison possible de la non intégration de xor builtins est peut être le fait que and et or retourne en résultat l’un de leur opérande. Avec xor c’est plus dur au mieux (sujet à interprétation) l’on a:
Bon l’opérateur not lui aussi retourne des booléens quelques soient l’opérande ; mais c’est peut être une explication car on pourrait vouloir d’autres politique de sortie pour xor!!!
C’est fou, en Ada, les “and” et “or” testent toutes les opérandes et on a deux autres opérateurs qui sont “and then” et “or else” qui font ce qui est fait en Python. J’ai jamais rien revu de la sorte dans un autre langage.
Une autre utilisation géniale du comportement de
or
c’est pour la gestion des valeurs de paramètres par défaut.D’après http://sametmax.com/id-none-et-bidouilleries-memoire-en-python/, il faudrait tester si une variable est None avec un “is None” au lieu de traiter comme un booléen mais c’est quand même super joli de faire: