python - switch - Forma pitónica para evitar las declaraciones "if x: return x"
switch python (18)
¿Has considerado simplemente escribir
if x: return x
all en una línea?
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
Esto no es menos repetitivo que lo que tenía, pero IMNSHO se lee un poco más suave.
Tengo un método que llama a otros 4 métodos en secuencia para verificar condiciones específicas, y regresa inmediatamente (sin verificar los siguientes) cada vez que uno devuelve algo Verdad.
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
Esto parece mucho código de equipaje. En lugar de cada instrucción if de 2 líneas, prefiero hacer algo como:
x and return x
Pero eso es inválido Python. ¿Me estoy perdiendo una solución simple y elegante aquí? Por cierto, en esta situación, esos cuatro métodos de verificación pueden ser costosos, por lo que no quiero llamarlos varias veces.
Alternativamente a la buena respuesta de Martijn, podría encadenar
or
.
Esto devolverá el primer valor verdadero, o
None
si no hay un valor verdadero:
def check_all_conditions():
return check_size() or check_color() or check_tone() or check_flavor() or None
Manifestación:
>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '''' or None
>>> x is None
True
De acuerdo con la ley de Curly , puede hacer que este código sea más legible dividiendo dos preocupaciones:
- ¿Qué cosas reviso?
- ¿Ha vuelto una cosa verdad?
en dos funciones:
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
Esto evita:
- estructuras lógicas complicadas
- líneas muy largas
- repetición
... conservando un flujo lineal y fácil de leer.
Probablemente también pueda encontrar nombres de funciones aún mejores, de acuerdo con su circunstancia particular, lo que lo hace aún más legible.
Efectivamente, la misma respuesta que timgeb, pero podría usar paréntesis para un mejor formato:
def check_all_the_things():
return (
one()
or two()
or five()
or three()
or None
)
Esta es una variante del primer ejemplo de Martijns. También utiliza el estilo de "colección de llamadas" para permitir cortocircuitos.
En lugar de un bucle, puede usar el incorporado.
conditions = (check_size, check_color, check_tone, check_flavor)
return any(condition() for condition in conditions)
Tenga en cuenta que
any
devuelve un valor booleano, por lo que si necesita el valor de retorno exacto del cheque, esta solución no funcionará.
any
no distinguirá entre
14
,
''red''
,
''sharp''
,
''spicy''
como valores de retorno, todos serán devueltos como
True
.
Esta manera está un poco fuera de la caja, pero creo que el resultado final es simple, legible y se ve bien.
La idea básica es
raise
una excepción cuando una de las funciones se evalúa como verdadera y devolver el resultado.
Así es como podría verse:
def check_conditions():
try:
assertFalsey(
check_size,
check_color,
check_tone,
check_flavor)
except TruthyException as e:
return e.trigger
else:
return None
Necesitará una función
assertFalsey
que
assertFalsey
una excepción cuando uno de los argumentos de la función llamada se evalúe como verdadero:
def assertFalsey(*funcs):
for f in funcs:
o = f()
if o:
raise TruthyException(o)
Lo anterior podría modificarse para proporcionar también argumentos para las funciones que se evaluarán.
Y, por supuesto, necesitará la
TruthyException
sí.
Esta excepción proporciona el
object
que activó la excepción:
class TruthyException(Exception):
def __init__(self, obj, *args):
super().__init__(*args)
self.trigger = obj
Puede convertir la función original en algo más general, por supuesto:
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)
Esto podría ser un poco más lento porque está utilizando una instrucción
if
y manejando una excepción.
Sin embargo, la excepción solo se maneja un máximo de una vez, por lo que el impacto en el rendimiento debe ser menor a menos que espere ejecutar la verificación y obtener un valor
True
muchos miles de veces.
Estoy bastante sorprendido de que nadie mencione el incorporado que está hecho para este propósito:
def check_all_conditions():
return any([
check_size(),
check_color(),
check_tone(),
check_flavor()
])
Tenga en cuenta que aunque esta implementación es probablemente la más clara, evalúa todas las comprobaciones incluso si la primera es
True
.
Si realmente necesita detenerse en la primera verificación fallida, considere usar
reduce
que se hace para convertir una lista a un valor 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])
: aplica la función de dos argumentos acumulativamente a los elementos de iterable, de izquierda a derecha, para reducir el iterable a un solo valor. El argumento izquierdo, x, es el valor acumulado y el argumento derecho, y, es el valor de actualización del iterable. Si el inicializador opcional está presente, se coloca antes de los elementos del iterable en el cálculo
En tu caso:
-
lambda a, f: a or f()
es la función que verifica que el acumulador ao la verificación actualf()
seaTrue
. Tenga en cuenta que sia
esTrue
,f()
no se evaluará. -
checks
contiene funciones de verificación (el elementof
de la lambda) -
False
es el valor inicial, de lo contrario no se realizaría ninguna comprobación y el resultado siempre seríaTrue
any
y
reduce
son herramientas básicas para la programación funcional.
Te recomiendo encarecidamente que los entrenes y que hagas un
map
que también es increíble.
He visto algunas implementaciones interesantes de declaraciones de cambio / caso con dictos en el pasado que me llevaron a esta respuesta.
Usando el ejemplo que ha proporcionado, obtendrá lo siguiente.
(Es una locura
using_complete_sentences_for_function_names
, así que
check_all_conditions
cambia su nombre a
status
. Ver (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 función select elimina la necesidad de llamar a cada
check_FUNCTION
dos veces, es decir, evita
check_FUNCTION() if check_FUNCTION() else next
agregar otra capa de función.
Esto es útil para funciones de larga duración.
Las lambdas en el dict retrasan la ejecución de sus valores hasta el ciclo while.
Como beneficio adicional, puede modificar el orden de ejecución e incluso omitir algunas de las pruebas alterando
k
y
s
por ejemplo,
k=''c'',s={''c'':''b'',''b'':None}
reduce el número de pruebas e invierte el orden de procesamiento original.
Los compañeros de tiempo pueden regatear el costo de agregar una o dos capas adicionales a la pila y el costo de la búsqueda de datos, pero parece más preocupado por la belleza del código.
Alternativamente, una implementación más simple podría ser la siguiente:
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
- Me refiero a esto no en términos de pep8 sino en términos de usar una palabra descriptiva concisa en lugar de una oración. Por supuesto, el OP puede estar siguiendo alguna convención de codificación, trabajando una base de código existente o no importa términos breves en su base de código.
Idealmente, volvería a escribir las funciones
check_
para devolver
True
o
False
lugar de un valor.
Sus cheques luego se convierten
if check_size(x):
return x
#etc
Suponiendo que su
x
no es inmutable, su función aún puede modificarla (aunque no pueden reasignarla), pero una función llamada
check
no debería modificarla de todos modos.
La forma pitónica es usar reducir (como alguien ya mencionó) o itertools (como se muestra a continuación), pero
me parece que simplemente usar un cortocircuito del operador
or
produce un código más claro
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
Me gusta @ timgeb''s.
Mientras tanto, me gustaría agregar que no es necesario expresar
None
en la declaración de
return
ya que se evalúa la recopilación de declaraciones separadas y se devuelve la primera no-cero, ninguna-vacía, ninguna-Ninguna y si no hay ninguna ¡entonces se devuelve
None
si hay
None
o no!
Entonces mi función
check_all_conditions()
ve así:
def check_all_conditions():
return check_size() or check_color() or check_tone() or check_flavor()
Usando
timeit
con
number=10**7
timeit
el tiempo de ejecución de una serie de sugerencias.
En aras de la comparación, acabo de utilizar la función
random.random()
para devolver una cadena o
None
basada en números aleatorios.
Aquí está el código completo:
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()
Y aquí están los resultados:
Bernard: 7.398444877040768
Martijn Pieters: 8.506569201346597
timgeb: 7.244275416364456
Reza: 6.982133448743038
Phinet: 7.925932800076634
Phil Frost: 11.924794811353031
O use
max
:
def check_all_conditions():
return max(check_size(), check_color(), check_tone(), check_flavor()) or None
Para mí, la mejor respuesta es la de @ phil-frost, seguida de @ wayne-werner.
Lo que me parece interesante es que nadie ha dicho nada sobre el hecho de que una función devolverá muchos tipos de datos diferentes, lo que hará que sea obligatorio realizar verificaciones en el tipo de x para realizar cualquier trabajo adicional.
Entonces mezclaría la respuesta de @ PhilFrost con la idea de mantener un solo tipo:
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
Observe que
x
se pasa como argumento, pero también
all_conditions
se usa como un generador pasado de funciones de verificación donde todos obtienen una
x
para verificar y devuelven
True
o
False
.
Al usar
func
con
all_conditions
como valor predeterminado, puede usar
assessed_x(x)
, o puede pasar un generador personalizado adicional a través de
func
.
De esa manera, obtienes
x
tan pronto como pasa un cheque, pero siempre será del mismo tipo.
Podrías usar un bucle:
conditions = (check_size, check_color, check_tone, check_flavor)
for condition in conditions:
result = condition()
if result:
return result
Esto tiene la ventaja adicional de que ahora puede hacer que el número de condiciones sea variable.
Puede usar
map()
+
filter()
(las versiones de Python 3, use las
versiones
future_builtins
en Python 2) para obtener el primer valor coincidente:
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)
pero si esto es más legible es discutible.
Otra opción es usar una expresión generadora:
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)
Si desea la misma estructura de código, ¡podría usar declaraciones ternarias!
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
Creo que esto se ve bien y claro si lo miras.
Manifestación:
Una ligera variación en el primer ejemplo de Martijns anterior, que evita el if dentro del bucle:
Status = None
for c in [check_size, check_color, check_tone, check_flavor]:
Status = Status or c();
return Status
Voy a saltar aquí y nunca he escrito una sola línea de Python, pero supongo que
if x = check_something(): return x
es válido?
si es así:
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
No lo cambies
Hay otras formas de hacerlo, como lo muestran las otras respuestas. Ninguno es tan claro como su código original.