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 (ne vérifiant pas les suivantes) chaque fois que l'on retourne 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 semble être beaucoup de code de bagages. Au lieu de chaque instruction if de 2 lignes, je préfère faire quelque chose comme:

x and return x

Mais C'est Python invalide. Est-ce qu'il me manque une solution simple et élégante ici? Incidemment, dans cette situation, ces quatre méthodes de vérification peuvent être coûteuses, donc je ne le fais pas envie de les appeler plusieurs fois.

210
demandé sur Martijn Pieters 2016-03-20 21:11:44

17 réponses

, Vous pouvez utiliser une boucle:

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

Ceci a l'avantage que vous pouvez maintenant faire le nombre de conditions variables.

Vous pouvez utiliser map() + filter() (le Python 3 versions, utilisez la future_builtins versions en 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 c'est plus lisible est discutable.

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

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)
269
répondu Martijn Pieters 2016-03-20 21:09:43

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

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
383
répondu timgeb 2016-03-21 08:29:35

Ne le changez pas

Il existe d'autres façons de le faire comme le montrent les différentes autres réponses. Aucun n'est aussi clair que votre code d'origine.

81
répondu Jack Aidley 2016-03-21 11:03:04

Dans la même réponse que timgeb, mais 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
    )
75
répondu Wayne Werner 2016-03-22 17:48:31

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

  • quelles choses dois-je vérifier?
  • Une chose est-elle 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 et facile à lire.

Vous pouvez probablement aussi trouver des noms de fonctions encore meilleurs, selon votre situation particulière, ce qui le rend encore plus lisible.

71
répondu Phil Frost 2016-03-23 01:38:57

Ceci est une variante de Martijns premier exemple. Il utilise également le style "collection of callables" afin de permettre un court-circuit.

, au Lieu d'une boucle, vous pouvez utiliser le builtin any.

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

Notez que any renvoie un booléen, donc si vous avez besoin de la valeur de retour exacte de la vérification, 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 retournées comme True.

40
répondu Leonhard 2016-03-21 09:14:00

Avez-vous envisagé d'é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 il se lit un peu plus lisse.

25
répondu zwol 2016-03-21 22:12:00

Je suis assez surpris que personne n'a mentionné le haut-any qui est fait pour cela:

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 devez vraiment vous arrêter à la première vérification échouée, envisagez d'utiliser reduce qui est fait 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]) : Appliquer la fonction de deux arguments cumulés pour les éléments de itératif, de gauche à droite, afin de réduire l'itérable à une valeur unique. La gauche argument, x, est la valeur accumulée et le bon argument, y est la mise à jour la valeur de l'objet iterable. Si l'initialiseur facultatif est présent, il est placé avant les éléments de l'itérable dans le calcul

Dans votre cas:

  • lambda a, f: a or f() est la fonction qui vérifie que soit l'accumulateur a ou sur la case f() est True. Notez que si a est True, f() ne sera pas évalué.
  • checks contient des fonctions de vérification (l'élément f du lambda)
  • {[15] } est la valeur initiale, sinon aucune vérification ne se produirait 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 ainsi que map ce qui est génial aussi!

23
répondu blint 2016-03-23 09:20:27

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 semble agréable et clair si vous regardez.

Démo:

Capture d'écran de l'exécution de

18
répondu Phinet 2016-03-21 00:14:15

Une légère variation sur Martijns premier exemple ci-dessus, qui évite le if à 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
répondu mathreadler 2016-03-21 03:33:11

Pour moi, la meilleure réponse est celle de @phil-gel, suivi par @wayne-werner.

Ce que je trouve intéressant, c'est que personne n'a rien dit sur le fait qu'une fonction retournera de nombreux types de données différents, ce qui rendra alors obligatoire de faire des vérifications sur le type de x lui-même pour faire d'autres travaux.

Donc je mélangerais la réponse de @ PhilFrost avec l'idée de garder 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 les x est passé comme argument, mais aussi all_conditions est utilisé comme un générateur passé de fonctions de vérification où toutes obtiennent un x à vérifier, et renvoient True ou False. En utilisant func avec all_conditions comme valeur par défaut, vous pouvez utiliser assessed_x(x), ou vous pouvez passer à un autre personnalisé générateur via func.

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

3
répondu juandesant 2016-03-29 09:11:08

Idéalement, je réécrirais les fonctions check_ pour retourner 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 n'est pas immuable, votre fonction peut toujours le modifier (bien qu'ils ne puissent pas le réaffecter) - mais une fonction appelée check ne devrait pas vraiment le modifier de toute façon.

2
répondu RoadieRich 2016-03-22 05:03:22

Cette façon est un peu en dehors de la boîte, mais je pense que le résultat final est simple, lisible, et semble agréable.

L'idée de base est de raise Une exception lorsque l'une des fonctions évalue comme véridique 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 truthy:

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

Ce qui précède pourrait être modifié de manière à fournir également des arguments pour la fonctions pour être évalué.

Et bien sûr, vous aurez besoin du TruthyException lui-même. Cette exception fournit le {[7] } qui a déclenché l'exception:

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

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

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. Cependant, l'exception n'est gérée qu'une seule fois, de sorte que le hit to performance doit être mineur sauf si vous prévoyez d'exécuter le vérifiez et obtenez une valeur True plusieurs milliers de fois.

2
répondu Rick Teachey 2016-03-23 20:40:10

La manière pythonique utilise reduce (comme quelqu'un déjà mentionné) ou itertools (comme indiqué ci-dessous), mais Il me semble que le simple fait d'utiliser un court-circuit de 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
1
répondu Dmitry Rubanovich 2016-04-02 02:47:11

J'aime @timgeb. en attendant, je voudrais ajouter que l'expression None dans l'instruction return n'est pas nécessaire car la collection d'instructions séparées or est évaluée et le premier none-zero, None-empty, none-None est retourné et s'il n'y en a pas alors None est retourné s'il y a 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()

Utiliser timeit avec number=10**7 j'ai regardé le temps d'exécution d'un certain nombre de suggestions. Pour l'amour de comparaison je viens d'utiliser la fonction random.random() Pour renvoyer une chaîne ou None basée sur des nombres aléatoires. Voici le code entier:

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 __name__ == '__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
1
répondu Reza Dodge 2017-03-30 15:50:06

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

Si oui:

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
répondu Richard87 2016-04-12 17:02:40

J'ai vu quelques implémentations intéressantes d'Instructions switch / case avec des dicts dans le passé qui m'ont conduit à cette réponse. En utilisant l'exemple que vous avez fourni, vous obtiendrez ce qui suit. (C'est de la folie using_complete_sentences_for_function_names, donc check_all_conditions est renommé status. Voir (1))

def status(k = 'a', s = {'a':'b','b':'c','c':'d','d':None}) :
  select = lambda next, test : test if test else next
  d = {'a': lambda : select(s['a'], check_size()  ),
       'b': lambda : select(s['b'], check_color() ),
       'c': lambda : select(s['c'], check_tone()  ),
       'd': lambda : select(s['d'], check_flavor())}
  while k in d : k = d[k]()
  return k

La fonction select élimine le besoin d'appeler chaque check_FUNCTION deux fois, c'est-à-dire que vous évitez check_FUNCTION() if check_FUNCTION() else next en ajoutant une autre couche de fonction. Ceci est utile pour les fonctions de longue durée. Les lambdas dans le dict retardent l'exécution de ses valeurs jusqu'à la boucle while.

En prime, vous pouvez modifier l'ordre d'exécution et même sauter certains des tests en modifiant k et s par exemple k='c',s={'c':'b','b':None} réduit le nombre de tests et inverse l'ordre de traitement d'origine.

Les boursiers timeit pourraient marchander sur le coût d'ajouter une ou deux couches supplémentaires à la pile et le coût de la recherche dict, mais vous semblez plus préoccupé par la beauté du code.

Une implémentation plus simple pourrait également être la suivante :

def status(k=check_size) :
  select = lambda next, test : test if test else next
  d = {check_size  : lambda : select(check_color,  check_size()  ),
       check_color : lambda : select(check_tone,   check_color() ),
       check_tone  : lambda : select(check_flavor, check_tone()  ),
       check_flavor: lambda : select(None,         check_flavor())}
  while k in d : k = d[k]()
  return k
  1. je veux dire ceci non pas en termes de pep8 mais en termes d'utilisation d'un mot descriptif concis à la place d'une phrase. Certes, L'OP peut suivre une convention de codage, travailler une base de code existante ou ne pas se soucier des termes laconiques dans leur base de code.
-2
répondu Carel 2016-03-27 17:38:18