web-dev-qa-db-fra.com

Manière pythonique d'éviter les instructions "if x: return x"

J'ai une méthode qui appelle 4 autres méthodes en séquence pour vérifier des conditions spécifiques, et retourne immédiatement (sans vérifier les suivantes) à chaque fois que l'on renvoie quelque chose de vrai.

def check_all_conditions():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

Cela ressemble à beaucoup de code de bagages. Au lieu de chaque déclaration de 2 lignes si, je préfère faire quelque chose comme:

x and return x

Mais ce n'est pas valide Python. Me manque-t-il une solution simple et élégante ici? Incidemment, dans cette situation, ces quatre méthodes de contrôle peuvent être coûteuses, je ne souhaite donc pas les appeler plusieurs fois.

217
Bernard

Vous pouvez utiliser une boucle:

conditions = (check_size, check_color, check_tone, check_flavor)
for condition in conditions:
    result = condition()
    if result:
        return result

Cela présente l’avantage supplémentaire que vous pouvez maintenant modifier le nombre de conditions.

Vous pouvez utiliser map() + filter() (les versions Python 3, utilisez les versions future_builtins) dans Python 2) pour obtenir la première valeur correspondante:

try:
    # Python 2
    from future_builtins import map, filter
except ImportError:
    # Python 3
    pass

conditions = (check_size, check_color, check_tone, check_flavor)
return next(filter(None, map(lambda f: f(), conditions)), None)

mais si cela est plus lisible est discutable.

Une autre option consiste à utiliser une expression génératrice:

conditions = (check_size, check_color, check_tone, check_flavor)
checks = (condition() for condition in conditions)
return next((check for check in checks if check), None)
277
Martijn Pieters

Alternativement à la bonne réponse de Martijn, vous pouvez enchaîner or. Cela retournera la première valeur de vérité, ou None s'il n'y a pas de valeur de vérité:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor() or None

Démo:

>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '' or None
>>> x is None
True
392
timgeb

ne le change pas

Comme le montrent les différentes réponses, il existe d'autres moyens de le faire. Aucun n'est aussi clair que votre code d'origine.

86
Jack Aidley

Dans la même réponse que timgeb, vous pouvez utiliser des parenthèses pour un formatage plus agréable:

def check_all_the_things():
    return (
        one()
        or two()
        or five()
        or three()
        or None
    )
82
Wayne Werner

Selon loi de Curly , vous pouvez rendre ce code plus lisible en scindant deux préoccupations:

  • Qu'est-ce que je vérifie?
  • Une chose est-elle redevenue vraie?

en deux fonctions:

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions():
    for condition in all_conditions():
        if condition:
            return condition
    return None

Cela évite:

  • structures logiques compliquées
  • très longues lignes
  • répétition

... tout en préservant un flux linéaire, facile à lire.

Vous pouvez probablement également proposer des noms de fonction encore meilleurs, en fonction de votre situation particulière, ce qui le rend encore plus lisible.

74
Phil Frost

Ceci est une variante du premier exemple de Martijns. Il utilise également le style "collection of callables" afin de permettre les courts-circuits.

Au lieu d'une boucle, vous pouvez utiliser la fonction intégrée any.

conditions = (check_size, check_color, check_tone, check_flavor)
return any(condition() for condition in conditions) 

Notez que any renvoie un booléen. Si vous avez besoin de la valeur de retour exacte du contrôle, cette solution ne fonctionnera pas. any ne fera pas la distinction entre 14, 'red', 'sharp', 'spicy' en tant que valeurs de retour, elles seront toutes renvoyées sous la forme True.

42
Leonhard

Avez-vous pensé à écrire if x: return x sur une seule ligne?

def check_all_conditions():
    x = check_size()
    if x: return x

    x = check_color()
    if x: return x

    x = check_tone()
    if x: return x

    x = check_flavor()
    if x: return x

    return None

Ce n'est pas moins répétitif que ce que vous aviez, mais IMNSHO le lit un peu plus facilement.

27
zwol

Je suis assez surpris que personne n'ait mentionné le --- any qui est créé à cet effet:

def check_all_conditions():
    return any([
        check_size(),
        check_color(),
        check_tone(),
        check_flavor()
    ])

Notez que bien que cette implémentation soit probablement la plus claire, elle évalue toutes les vérifications même si la première est True.


Si vous avez vraiment besoin de vous arrêter à la première vérification, envisagez d'utiliser reduce pour convertir une liste en une valeur simple:

def check_all_conditions():
    checks = [check_size, check_color, check_tone, check_flavor]
    return reduce(lambda a, f: a or f(), checks, False)

reduce(function, iterable[, initializer]): Applique la fonction de deux arguments de manière cumulative aux éléments d'iterable, de gauche à droite, afin de réduire l'iterable à une valeur unique. L'argument de gauche, x, est la valeur accumulée et l'argument de droite, y, la valeur de mise à jour de l'itérable. Si l'initialiseur optionnel est présent, il est placé avant les éléments de l'itérable dans le calcul

Dans ton cas:

  • lambda a, f: a or f() est la fonction qui vérifie que l'accumulateur a ou le contrôle en cours f() est True. Notez que si a est True, f() ne sera pas évalué.
  • checks contient des fonctions de contrôle (l'élément f du lambda)
  • False est la valeur initiale, sinon aucune vérification ne serait effectuée et le résultat serait toujours True

any et reduce sont des outils de base pour la programmation fonctionnelle. Je vous encourage fortement à les former aussi bien que map qui est génial aussi!

24
blint

Si vous voulez la même structure de code, vous pouvez utiliser des instructions ternaires!

def check_all_conditions():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

Je pense que cela a l'air bien et clair si vous le regardez.

Démo:

Screenshot of it running

19
Phinet

Pour moi, la meilleure réponse est celle de @ phil-frost, suivie de @ wayne-werner.

Ce que je trouve intéressant, c’est que personne n’a encore rien dit sur le fait qu’une fonction renvoie de nombreux types de données, ce qui obligera à vérifier le type de x lui-même pour effectuer des travaux supplémentaires.

Je mélangerais donc la réponse de @ PhilFrost avec l'idée de conserver un seul type:

def all_conditions(x):
    yield check_size(x)
    yield check_color(x)
    yield check_tone(x)
    yield check_flavor(x)

def assessed_x(x,func=all_conditions):
    for condition in func(x):
        if condition:
            return x
    return None

Notez que x est passé en tant qu'argument, mais all_conditions est également utilisé en tant que générateur passé de fonctions de vérification, où toutes doivent vérifier x et renvoyer True ou False. En utilisant func avec all_conditions comme valeur par défaut, vous pouvez utiliser assessed_x(x), ou vous pouvez transmettre un autre générateur personnalisé via func.

De cette façon, vous obtenez x dès qu'un chèque est passé, mais ce sera toujours le même type.

5
juandesant

Idéalement, je réécrirais les fonctions check_ pour renvoyer True ou False plutôt qu'une valeur. Vos chèques deviennent alors

if check_size(x):
    return x
#etc

En supposant que votre x ne soit pas immuable, votre fonction peut toujours la modifier (bien qu’elle ne puisse pas la réaffecter) - mais une fonction appelée check ne devrait pas vraiment la modifier de toute façon.

4
RoadieRich

Une légère variation sur le premier exemple de Martijns ci-dessus, qui évite le si à l'intérieur de la boucle:

Status = None
for c in [check_size, check_color, check_tone, check_flavor]:
  Status = Status or c();
return Status
3
mathreadler

La méthode Pythonic utilise soit la réduction (comme déjà mentionné), soit les outils (comme indiqué ci-dessous), mais il me semble que le simple fait de court-circuiter l'opérateur or produit un code plus clair

from itertools import imap, dropwhile

def check_all_conditions():
    conditions = (check_size,\
        check_color,\
        check_tone,\
        check_flavor)
    results_gen = dropwhile(lambda x:not x, imap(lambda check:check(), conditions))
    try:
        return results_gen.next()
    except StopIteration:
        return None
2
Dmitry Rubanovich

J'aime @ Timgeb's. Entre-temps, j'aimerais ajouter que l'expression de None dans l'instruction return n'est pas nécessaire, car la collection d'instructions séparées or est évaluée et les premières valeurs non nul, non vide, none-None est retourné et s'il n'y en a pas, alors None est renvoyé, qu'il y ait un None ou pas!

Donc, ma fonction check_all_conditions() ressemble à ceci:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor()

Utilisation de timeit avec number=10**7 J'ai examiné la durée d'exécution d'un certain nombre de suggestions. À des fins de comparaison, je viens d’utiliser la fonction random.random() pour renvoyer une chaîne ou None à partir de nombres aléatoires. Voici le code complet:

import random
import timeit

def check_size():
    if random.random() < 0.25: return "BIG"

def check_color():
    if random.random() < 0.25: return "RED"

def check_tone():
    if random.random() < 0.25: return "SOFT"

def check_flavor():
    if random.random() < 0.25: return "SWEET"

def check_all_conditions_Bernard():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

def check_all_Martijn_Pieters():
    conditions = (check_size, check_color, check_tone, check_flavor)
    for condition in conditions:
        result = condition()
        if result:
            return result

def check_all_conditions_timgeb():
    return check_size() or check_color() or check_tone() or check_flavor() or None

def check_all_conditions_Reza():
    return check_size() or check_color() or check_tone() or check_flavor()

def check_all_conditions_Phinet():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions_Phil_Frost():
    for condition in all_conditions():
        if condition:
            return condition

def main():
    num = 10000000
    random.seed(20)
    print("Bernard:", timeit.timeit('check_all_conditions_Bernard()', 'from __main__ import check_all_conditions_Bernard', number=num))
    random.seed(20)
    print("Martijn Pieters:", timeit.timeit('check_all_Martijn_Pieters()', 'from __main__ import check_all_Martijn_Pieters', number=num))
    random.seed(20)
    print("timgeb:", timeit.timeit('check_all_conditions_timgeb()', 'from __main__ import check_all_conditions_timgeb', number=num))
    random.seed(20)
    print("Reza:", timeit.timeit('check_all_conditions_Reza()', 'from __main__ import check_all_conditions_Reza', number=num))
    random.seed(20)
    print("Phinet:", timeit.timeit('check_all_conditions_Phinet()', 'from __main__ import check_all_conditions_Phinet', number=num))
    random.seed(20)
    print("Phil Frost:", timeit.timeit('check_all_conditions_Phil_Frost()', 'from __main__ import check_all_conditions_Phil_Frost', number=num))

if __== '__main__':
    main()

Et voici les résultats:

Bernard: 7.398444877040768
Martijn Pieters: 8.506569201346597
timgeb: 7.244275416364456
Reza: 6.982133448743038
Phinet: 7.925932800076634
Phil Frost: 11.924794811353031
2
Reza Dodge

Cette façon est un peu en dehors de la boîte, mais je pense que le résultat final est simple, lisible et a l’air sympa.

L'idée de base est de raise une exception lorsque l'une des fonctions est évaluée comme une vérité et renvoie le résultat. Voici à quoi cela pourrait ressembler:

def check_conditions():
    try:
        assertFalsey(
            check_size,
            check_color,
            check_tone,
            check_flavor)
    except TruthyException as e:
        return e.trigger
    else:
        return None

Vous aurez besoin d'une fonction assertFalsey qui déclenche une exception lorsque l'un des arguments de la fonction appelée est évalué comme une vérité:

def assertFalsey(*funcs):
    for f in funcs:
        o = f()
        if o:
            raise TruthyException(o)

Ce qui précède pourrait être modifié afin de fournir également des arguments pour les fonctions à évaluer.

Et bien sûr, vous aurez besoin de la TruthyException elle-même. Cette exception fournit la object qui a déclenché l'exception:

class TruthyException(Exception):
    def __init__(self, obj, *args):
        super().__init__(*args)
        self.trigger = obj

Vous pouvez bien sûr transformer la fonction d'origine en quelque chose de plus général:

def get_truthy_condition(*conditions):
    try:
        assertFalsey(*conditions)
    except TruthyException as e:
        return e.trigger
    else:
        return None

result = get_truthy_condition(check_size, check_color, check_tone, check_flavor)

Cela peut être un peu plus lent car vous utilisez à la fois une instruction if et une exception. Toutefois, l'exception étant gérée au maximum une fois, l'impact sur les performances doit être mineur, sauf si vous souhaitez exécuter le contrôle et obtenir une valeur True plusieurs milliers de fois.

2
Rick Teachey

Je vais sauter ici et je n'ai jamais écrit une seule ligne de Python, mais je suppose que if x = check_something(): return x est valide?

si c'est le cas:

def check_all_conditions():

    if (x := check_size()): return x
    if (x := check_color()): return x
    if (x := check_tone()): return x
    if (x := check_flavor()): return x

    return None
0
Richard87

Ou utilisez max:

def check_all_conditions():
    return max(check_size(), check_color(), check_tone(), check_flavor()) or None
0
U10-Forward