Extraño uso de “y” / “o” ​​operador

Estoy tratando de aprender python y encontré un código que es agradable y corto pero que no tiene ningún sentido.

el contexto fue:

def fn(*args): return len(args) and max(args)-min(args) 

Entiendo lo que está haciendo, pero ¿por qué Python hace esto, es decir, devuelve el valor en lugar de Verdadero / Falso?

 10 and 7-2 

devuelve 5. De forma similar, cambiar el y para o dará lugar a un cambio en la funcionalidad. Asi que

 10 or 7 - 2 

Volvería 10.

¿Es este estilo legítimo / confiable, o hay algún error en esto?

TL; DR

Comenzamos por resumir los dos comportamientos de los dos operadores lógicos and y or . Estos modismos formarán la base de nuestra discusión a continuación.

and

Devuelve el primer valor de Falsey si hay alguno, de lo contrario, devuelve el último valor en la expresión.

or

Devuelve el primer valor de Verdad si hay alguno, de lo contrario, devuelve el último valor en la expresión.

El comportamiento también se resume en los documentos , especialmente en esta tabla:

introduzca la descripción de la imagen aquí

El único operador que devuelve un valor booleano independientemente de sus operandos es el operador not .


Evaluaciones de “Verdad” y “Verdad”

La statement

 len(args) and max(args) - min(args) 

Es una forma concisa muy pythonica (y posiblemente menos legible) de decir “si args no está vacío, devuelva el resultado de max(args) - min(args) “, de lo contrario, devuelva 0 . En general, es una representación más concisa de una expresión if-else . Por ejemplo,

 exp1 and exp2 

Debería (a grandes rasgos) traducir a:

 r1 = exp1 if not r1: r1 = exp2 

O equivalente,

 r1 = exp1 if exp1 else exp2 

Donde exp1 y exp2 son objetos de python arbitrarios, o expresiones que devuelven algún objeto. La clave para entender los usos de los operadores lógicos and / or aquí es entender que no están restringidos a operar o devolver valores booleanos. Cualquier objeto con un valor de verdad puede ser probado aquí. Esto incluye int , str , list , dict , tuple , set , NoneType y objetos definidos por el usuario. Las reglas de cortocircuito también se aplican.

Pero ¿qué es la verdad?
Se refiere a cómo se evalúan los objetos cuando se usan en expresiones condicionales. @Patrick Haugh resume la veracidad muy bien en este post .

Todos los valores se consideran “veraces”, excepto los siguientes, que son “falsos”:

  • None
  • False
  • 0
  • 0.0
  • 0j
  • Decimal(0)
  • Fraction(0, 1)
  • [] – una list vacía
  • {} – un dict vacío
  • () – una tuple vacía
  • '' – un str vacío
  • b'' – un bytes vacío
  • set() – un set vacío
  • un range vacío, como el range(0)
  • objetos para los cuales
    • obj.__bool__() devuelve False
    • obj.__len__() devuelve 0

Un valor “verdadero” satisfará la verificación realizada por las declaraciones if o while . Usamos “truthy” y “falsy” para diferenciarnos de los valores bool True y False .


Como and funciona

Nos basamos en la pregunta de OP como parte de una discusión sobre cómo estos operadores en estos casos.

Dada una función con la definición.

 def foo(*args): ... 

¿Cómo devuelvo la diferencia entre el valor mínimo y máximo en una lista de cero o más argumentos?

Encontrar el mínimo y el máximo es fácil (¡use las funciones incorporadas!). La única pega aquí es manejar adecuadamente el caso de la esquina donde la lista de argumentos podría estar vacía (por ejemplo, llamando a foo() ). Podemos hacer ambas cosas en una sola línea gracias al operador and :

 def foo(*args): return len(args) and max(args) - min(args) 

 foo(1, 2, 3, 4, 5) # 4 foo() # 0 

Dado que and se utiliza, la segunda expresión también debe evaluarse si la primera es True . Tenga en cuenta que, si la primera expresión se evalúa para ser veraz, el valor de retorno siempre es el resultado de la segunda expresión . Si la primera expresión se evalúa como Falsey, entonces el resultado devuelto es el resultado de la primera expresión.

En la función anterior, si foo recibe uno o más argumentos, len(args) es mayor que 0 (un número positivo), por lo que el resultado devuelto es max(args) - min(args) . OTOH, si no se pasan argumentos, len(args) es 0 que es Falsey, y se devuelve 0 .

Tenga en cuenta que una forma alternativa de escribir esta función sería:

 def foo(*args): if not len(args): return 0 return max(args) - min(args) 

O, más concisamente,

 def foo(*args): return 0 if not args else max(args) - min(args) 

Por supuesto, ninguna de estas funciones realiza una comprobación de tipo, por lo que, a menos que confíe completamente en la entrada provista, no confíe en la simplicidad de estas construcciones.


Como or funciona

Explico el funcionamiento de or de una manera similar con un ejemplo artificial.

Dada una función con la definición.

 def foo(*args): ... 

¿Cómo completarías foo para devolver todos los números de más de 9000 ?

Usamos or para manejar el caso de la esquina aquí. Definimos foo como:

 def foo(*args): return [x for x in args if x > 9000] or 'No number over 9000!' foo(9004, 1, 2, 500) # [9004] foo(1, 2, 3, 4) # 'No number over 9000!' 

foo realiza una filtración en la lista para retener todos los números de más de 9000 . Si existen tales números, el resultado de la comprensión de la lista es una lista no vacía que es Verdad, por lo que se devuelve (cortocircuito en acción aquí). Si no existen tales números, entonces el resultado de la lista comp es [] que es Falsey. Así que la segunda expresión ahora se evalúa (una cadena no vacía) y se devuelve.

Usando condicionales, podríamos reescribir esta función como,

 def foo(*args): r = [x for x in args if x > 9000] if not r: return 'No number over 9000!' return r 

Como antes, esta estructura es más flexible en términos de manejo de errores.

Citar de documentos de Python

Tenga en cuenta que ni and ni or restringen el valor y el tipo que devuelven a False y True , sino que devuelven el último argumento evaluado . Esto a veces es útil, por ejemplo, si s es una cadena que debe reemplazarse por un valor predeterminado si está vacía, la expresión s or 'foo' produce el valor deseado.

Entonces, así es como Python fue diseñado para evaluar las expresiones booleanas y la documentación anterior nos da una idea de por qué lo hicieron así.

Para obtener un valor booleano simplemente encuéntralo.

 return bool(len(args) and max(args)-min(args)) 

¿Por qué?

Cortocircuito.

Por ejemplo:

 2 and 3 # Returns 3 because 2 is Truthy so it has to check 3 too 0 and 3 # Returns 0 because 0 is Falsey and there's no need to check 3 at all 

Lo mismo vale para or también, es decir, devolverá la expresión que es Verdadero tan pronto como la encuentre, ya que evaluar el rest de la expresión es redundante.

En lugar de devolver el hardcore True o False , Python devuelve Truthy o Falsey , que de todos modos se van a evaluar como True o False . Podría usar la expresión tal como está, y seguirá funcionando.


Para saber qué es Truthy y Falsey , verifique la respuesta de Patrick Haugh

y y o ejecutan la lógica booleana, pero devuelven uno de los valores reales cuando se comparan. Al usar y , los valores se evalúan en un contexto booleano de izquierda a derecha. 0, ”, [], (), {} y None son falsos en un contexto booleano; todo lo demás es verdad

Si todos los valores son verdaderos en un contexto booleano, y devuelve el último valor.

 >>> 2 and 5 5 >>> 2 and 5 and 10 10 

Si algún valor es falso en un contexto booleano y devuelve el primer valor falso.

 >>> '' and 5 '' >>> 2 and 0 and 5 0 

Entonces el código

 return len(args) and max(args)-min(args) 

devuelve el valor de max(args)-min(args) cuando hay args; de lo contrario, devuelve len(args) que es 0.

¿Es este estilo legítimo / confiable, o hay algún error en esto?

Esto es legítimo, es una evaluación de cortocircuito donde se devuelve el último valor.

Usted proporciona un buen ejemplo. La función devolverá 0 si no se pasan los argumentos, y el código no tiene que verificar un caso especial de no pasar los argumentos.

Otra forma de usar esto, es predeterminar los argumentos de None a una primitiva mutable, como una lista vacía:

 def fn(alist=None): alist = alist or [] .... 

Si se pasa algún valor no verídico a una lista, se establece de manera predeterminada en una lista vacía, una forma práctica de evitar una sentencia if y la trampa del argumento predeterminado mutable

Gotchas

Sí, hay algunas trampas.

fn() == fn(3) == fn(4, 4)

Primero, si fn devuelve 0 , no puede saber si fue llamado sin ningún parámetro, con un parámetro o con múltiples parámetros iguales:

 >>> fn() 0 >>> fn(3) 0 >>> fn(3, 3, 3) 0 

¿Qué significa fn ?

Entonces, Python es un lenguaje dynamic. No se especifica en ningún lugar qué hace fn , cuál debería ser su entrada y cómo debería ser su salida. Por lo tanto, es muy importante nombrar la función correctamente. Del mismo modo, los argumentos no tienen que ser llamados args . delta(*numbers) o calculate_range(*numbers) podrían describir mejor lo que se supone que hace la función.

Errores de argumento

Finalmente, se supone que el operador lógico and para evitar que la función falle si se llama sin ningún argumento. Aún así falla si algún argumento no es un número, sin embargo:

 >>> fn('1') Traceback (most recent call last): File "", line 1, in  File "", line 2, in fn TypeError: unsupported operand type(s) for -: 'str' and 'str' >>> fn(1, '2') Traceback (most recent call last): File "", line 1, in  File "", line 2, in fn TypeError: '>' not supported between instances of 'str' and 'int' >>> fn('a', 'b') Traceback (most recent call last): File "", line 1, in  File "", line 2, in fn TypeError: unsupported operand type(s) for -: 'str' and 'str' 

Posible alternativa

Aquí hay una forma de escribir la función de acuerdo con la sección “Más fácil pedir perdón que permiso”. principio :

 def delta(*numbers): try: return max(numbers) - min(numbers) except TypeError: raise ValueError("delta should only be called with numerical arguments") from None except ValueError: raise ValueError("delta should be called with at least one numerical argument") from None 

Como ejemplo:

 >>> delta() Traceback (most recent call last): File "", line 1, in  File "", line 7, in delta ValueError: delta should be called with at least one numerical argument >>> delta(3) 0 >>> delta('a') Traceback (most recent call last): File "", line 1, in  File "", line 5, in delta ValueError: delta should only be called with numerical arguments >>> delta('a', 'b') Traceback (most recent call last): File "", line 1, in  File "", line 5, in delta ValueError: delta should only be called with numerical arguments >>> delta('a', 3) Traceback (most recent call last): File "", line 1, in  File "", line 5, in delta ValueError: delta should only be called with numerical arguments >>> delta(3, 4.5) 1.5 >>> delta(3, 5, 7, 2) 5 

Si realmente no desea generar una excepción cuando se llama a delta sin ningún argumento, podría devolver algún valor que de otra forma no sería posible (por ejemplo, -1 o None ):

 >>> def delta(*numbers): ... try: ... return max(numbers) - min(numbers) ... except TypeError: ... raise ValueError("delta should only be called with numerical arguments") from None ... except ValueError: ... return -1 # or None ... >>> >>> delta() -1 

¿Es este estilo legítimo / confiable, o hay algún error en esto?

Me gustaría agregar a esta pregunta que no solo es legítima y confiable, sino que también es extremadamente práctica. Aquí hay un ejemplo simple:

 >>>example_list = [] >>>print example_list or 'empty list' empty list 

Por lo tanto, realmente puede utilizarlo en su ventaja. Para ser conciente así es como lo veo:

Or operador

Python’s or operator devuelve el primer valor de Verdad-y, o el último valor, y se detiene

And operador

Python’s and operador devuelven el primer valor False-y, o el último valor, y se detienen

Entre bastidores

En Python, todos los números se interpretan como True excepción de 0. Por lo tanto, diciendo:

 0 and 10 

es lo mismo que:

 False and True 

Lo que es claramente False . Por eso es lógico que devuelva 0

Sí. Este es el comportamiento correcto y la comparación.

Al menos en Python, A and B devuelven B si A es esencialmente True incluso si A NO es Nulo, NO None NO es un contenedor vacío (como una list vacía, dict , etc.). A se devuelve IFF A es esencialmente False o None o Vacío o Nulo.

Por otro lado, A or B devuelve A si A es esencialmente True incluso si A NO es Nulo, NO None NO es un contenedor vacío (como una list vacía, dict , etc.), de lo contrario, devuelve B

Es fácil no notar (o pasar por alto) este comportamiento porque, en Python, cualquier objeto que non-null no vacío que se evalúe como Verdadero se trata como un valor booleano.

Por ejemplo, todo lo siguiente imprimirá “Verdadero”

 if [102]: print "True" else: print "False" if "anything that is not empty or None": print "True" else: print "False" if {1, 2, 3}: print "True" else: print "False" 

Por otro lado, todo lo siguiente imprimirá “Falso”

 if []: print "True" else: print "False" if "": print "True" else: print "False" if set ([]): print "True" else: print "False"