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.
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)
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
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.
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
)
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.
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
.
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.
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'accumulateura
ou sur la casef()
estTrue
. Notez que sia
estTrue
,f()
ne sera pas évalué. -
checks
contient des fonctions de vérification (l'élémentf
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!
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:
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
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.
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.
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.
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
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
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
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
- 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.