Quelques astuces à propos de and et or


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.

19 thoughts on “Quelques astuces à propos de and et or

  • Paradox

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

  • Fred

    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 si pt é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")

    • Paradox

      Non, Fred, sauf (grosse) erreur de ma part en C/C++, ces opérations (&&, ||) sont binaires et évaluent toute l’expression du test.

  • Xavier Combelle

    @Fred le && du bash fait bien de l’évaluation paresseuse

  • entwanne

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

  • TD

    Y a-t-il une raison pour laquelle il n’existe pas d’opérateur xor built-in en Python ?

  • Sam Post author

    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é.

  • Migwel

    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

  • Fred

    @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 ;)

  • kontre

    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

  • nabellaleen

    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’]

  • Abject

    @Sam: c’est que les qu’en Python ==> c’est qu’en Python non ?

    Super intéressant.

  • Samson

    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:

    def xor(x, y):
        return (x and not y and x) or (not x and y)
    
    for x in ([], ["Y"]):
    
        for y in (0.0, 3.14):
            print "{0} xor {1} = {2}".format(x, y, xor(x, y))
        
    [] xor 0.0 = 0.0
    [] xor 3.14 = 3.14
    ['Y'] xor 0.0 = ['Y']
    ['Y'] xor 3.14 = False
    

    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!!!

  • Fritz

    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.

  • cladmi

    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:

    >>> def call_prog(args=None):
    ...     args = args or []
    ...     cmd = ['program'] + args
    ...     print cmd
    ...     # subprocess.call(cmd)
    ... 
    >>> call_prog()
    ['program']
    >>> call_prog(None)
    ['program']
    >>> call_prog(['arg1', 'arg2'])
    ['program', 'arg1', 'arg2']
    
    

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.