Defina una expresión lambda que genere una excepción

137

¿Cómo puedo escribir una expresión lambda que sea equivalente a:

def x():
    raise Exception()

Lo siguiente no está permitido:

y = lambda : raise Exception()
Thomas Jung
fuente
2
Entonces no puedes hacer eso. Utiliza funciones normales.
DrTyrsa
1
¿Cuál es el punto de dar un nombre a una función anónima?
John La Rooy el
2
@gnibbler Puede usar el nombre para referirse a la función. y () es más fácil de usar que (lambda: 0) () en REPL.
Thomas Jung
Entonces, ¿cuál es la ventaja de y=lambda...más de def y:entonces?
John La Rooy
@gnibbler Algún contexto: quería definir una función def g (f, e) que llame a f en el caso feliz y e si se detecta un error. Dependiendo del escenario, e podría generar una excepción o devolver algún valor válido. Para usar g, quería escribir g (lambda x: x * 2, lambda e: raise e) o alternativamente g (lambda x: x * 2, lambda e: 0).
Thomas Jung

Respuestas:

169

Hay más de una forma de pelar un Python:

y = lambda: (_ for _ in ()).throw(Exception('foobar'))

Lambdas acepta declaraciones. Dado que raise exes una declaración, podría escribir un recaudador de propósito general:

def raise_(ex):
    raise ex

y = lambda: raise_(Exception('foobar'))

Pero si su objetivo es evitar un def, obviamente esto no es suficiente. Sin embargo, le permite generar excepciones condicionalmente, por ejemplo:

y = lambda x: 2*x if x < 10 else raise_(Exception('foobar'))

Alternativamente, puede generar una excepción sin definir una función con nombre. Todo lo que necesitas es un estómago fuerte (y 2.x para el código dado):

type(lambda:0)(type((lambda:0).func_code)(
  1,1,1,67,'|\0\0\202\1\0',(),(),('x',),'','',1,''),{}
)(Exception())

Y una solución fuerte para el estómago python3 :

type(lambda: 0)(type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b''),{}
)(Exception())

Gracias @WarrenSpencer por señalar una respuesta muy simple, si no le importa qué excepción se plantea: y = lambda: 1/0.

Marcelo Cantos
fuente
117
Dios mío, ¿qué arte oscuro es?
CodeColorist
11
Si no le importa qué tipo de excepción se produce, el siguiente también funciona: lambda: 1 / 0. Simplemente terminarás con un ZeroDivisionError en lugar de una excepción normal. Tenga en cuenta que si se permite que la excepción se propague, puede parecer extraño que alguien depure su código para comenzar a ver un montón de ZeroDivisionErrors.
Warren Spencer
Gran solución @WarrenSpencer. La mayoría del código no tiene muchos errores de división cero, por lo que es tan distintivo como si pudieras elegir el tipo tú mismo.
jwg
2
y = 1/0es una solución súper inteligente si el tipo de excepción es irrelevante
Saher Ahwal
3
¿Alguien puede hablarnos sobre lo que realmente está sucediendo en las soluciones de 'arte oscuro / estómago fuerte'?
decreta
56

Qué tal si:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
fuente
12
Es bastante hacky, pero para escribir pruebas en las que desea burlarse de funciones, ¡esto funciona bien!
Kannan Ekanath
8
Funciona pero no deberías hacerlo.
augurar
1
Esto no funciona para mí, obtengo un SyntaxErrorPython 2.7.11.
Nick Sweeting
También recibo el error anterior (SyntaxError) en Python 2.7.5
Dinesh
1
Esto es específico de Python 3, sin embargo, no creo que Python 2 lo permita.
Saher Ahwal
16

En realidad, hay una manera, pero es muy artificial.

Puede crear un objeto de código utilizando la compile()función incorporada. Esto le permite usar la raisedeclaración (o cualquier otra declaración, para el caso), pero plantea otro desafío: ejecutar el objeto de código. La forma habitual sería usar la execdeclaración, pero eso lo lleva de regreso al problema original, es decir, que no puede ejecutar declaraciones en un lambda(o un eval(), para el caso).

La solución es un hack. Las llamadas como el resultado de una lambdadeclaración tienen un atributo __code__, que en realidad se puede reemplazar. Entonces, si crea un invocable y reemplaza su __code__valor con el objeto de código de arriba, obtendrá algo que puede evaluarse sin usar declaraciones. Sin embargo, lograr todo esto da como resultado un código muy oscuro:

map(lambda x, y, z: x.__setattr__(y, z) or x, [lambda: 0], ["__code__"], [compile("raise Exception", "", "single"])[0]()

Lo anterior hace lo siguiente:

  • la compile()llamada crea un objeto de código que genera la excepción;

  • las lambda: 0declaraciones de un desembolsadas que no hace más que devolver el valor 0 - esto se utiliza para ejecutar el objeto código más adelante;

  • el lambda x, y, zcrea una función que llama al __setattr__método del primer argumento con el resto de argumentos, y devuelve el primer argumento! Esto es necesario, porque en __setattr__sí mismo regresa None;

  • la map()llamada toma el resultado de lambda: 0, y el uso de lambda x, y, zreemplaza su __code__objeto con el resultado de la compile()llamada. El resultado de esta operación de mapa es una lista con una entrada, la que devuelve lambda x, y, z, por lo que necesitamos esto lambda: si lo __setattr__usáramos de inmediato, ¡perderíamos la referencia al lambda: 0objeto!

  • finalmente, map()se ejecuta el primer (y único) elemento de la lista devuelto por la llamada, lo que da como resultado que se llame al objeto de código, lo que finalmente genera la excepción deseada.

Funciona (probado en Python 2.6), pero definitivamente no es bonito.

Una última nota: si tiene acceso al typesmódulo (lo que requeriría usar la importdeclaración antes que su eval), entonces puede acortar un poco este código: usando types.FunctionType()puede crear una función que ejecutará el objeto de código dado, por lo que ganó No necesita el truco de crear una función ficticia lambda: 0y reemplazar el valor de su __code__atributo.

Michael Scarpa
fuente
15

Si todo lo que desea es una expresión lambda que genere una excepción arbitraria, puede lograr esto con una expresión ilegal. Por ejemplo, lambda x: [][0]intentará acceder al primer elemento en una lista vacía, lo que generará un IndexError.

TENGA EN CUENTA : este es un truco, no una característica. No use esto en ningún código (que no sea de código de golf) que otro ser humano pueda ver o usar.

Kyle Strand
fuente
En mi caso me sale: TypeError: <lambda>() takes exactly 1 positional argument (2 given). ¿Estás seguro del IndexError?
Jovik
44
Sí. ¿Quizás proporcionaste el número incorrecto de argumentos? Si necesita una función lambda que pueda tomar cualquier número de argumentos, use lambda *x: [][0]. (La versión original solo toma un argumento; para ningún argumento, use lambda : [][0]; para dos, use lambda x,y: [][0]; etc.)
Kyle Strand
3
He ampliado esto un poco: lambda x: {}["I want to show this message. Called with: %s" % x] Produce: KeyError: 'I want to show this message. Called with: foo'
ErlVolton
@ErlVolton Clever! Aunque usar esto en cualquier lugar, excepto en un guión único, parece una idea terrible ...
Kyle Strand
Estoy usando temporalmente pruebas unitarias para un proyecto en el que no me he molestado en burlarme de mi registrador. Se eleva si intenta registrar un error o crítico. Entonces ... Sí terrible, aunque consensuado :)
ErlVolton
10

Me gustaría dar una explicación de la ACTUALIZACIÓN 3 de la respuesta proporcionada por Marcelo Cantos:

type(lambda: 0)(type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b''),{}
)(Exception())

Explicación

lambda: 0es una instancia de la builtins.functionclase
type(lambda: 0)es la builtins.functionclase
(lambda: 0).__code__Es un codeobjeto.
Un codeobjeto es un objeto que contiene el bytecode compilado entre otras cosas. Se define aquí en CPython https://github.com/python/cpython/blob/master/Include/code.h . Sus métodos se implementan aquí https://github.com/python/cpython/blob/master/Objects/codeobject.c . Podemos ejecutar la ayuda en el objeto de código:

Help on code object:

class code(object)
 |  code(argcount, kwonlyargcount, nlocals, stacksize, flags, codestring,
 |        constants, names, varnames, filename, name, firstlineno,
 |        lnotab[, freevars[, cellvars]])
 |  
 |  Create a code object.  Not for the faint of heart.

type((lambda: 0).__code__)es la clase de código
Entonces cuando decimos

type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b'')

Estamos llamando al constructor del objeto de código con los siguientes argumentos:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • stacksize = 1
  • banderas = 67
  • cadena de códigos = b '| \ 0 \ 202 \ 1 \ 0'
  • constantes = ()
  • nombres = ()
  • varnames = ('x',)
  • filename = ''
  • nombre = ''
  • firstlineno = 1
  • lnotab = b ''

Puede leer sobre lo que significan los argumentos en la definición de PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . El valor de 67 para el flagsargumento es, por ejemplo CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE.

El argumento más importante es el codestringque contiene códigos de operación de instrucciones. Veamos a qué se refieren.

>>> import dis
>>> dis.dis(b'|\0\202\1\0')
          0 LOAD_FAST                0 (0)
          2 RAISE_VARARGS            1
          4 <0>

La documentación de los códigos de operación se puede encontrar aquí https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . El primer byte es el código de operación para LOAD_FAST, el segundo byte es su argumento, es decir, 0.

LOAD_FAST(var_num)
    Pushes a reference to the local co_varnames[var_num] onto the stack.

Entonces empujamos la referencia a xla pila. El varnameses una lista de cadenas que contiene solo 'x'. Llevaremos el único argumento de la función que estamos definiendo a la pila.

El siguiente byte es el código de operación para RAISE_VARARGSy el siguiente byte es su argumento, es decir, 1.

RAISE_VARARGS(argc)
    Raises an exception using one of the 3 forms of the raise statement, depending on the value of argc:
        0: raise (re-raise previous exception)
        1: raise TOS (raise exception instance or type at TOS)
        2: raise TOS1 from TOS (raise exception instance or type at TOS1 with __cause__ set to TOS)

El TOS es el mejor de la pila. Dado que empujamos el primer argumento ( x) de nuestra función a la pila y argces 1, elevaremos el xsi es una instancia de excepción o haremos una instancia dex y lo elevaremos de otra manera.

El último byte, es decir, 0 no se utiliza. No es un código de operación válido. Bien podría no estar allí.

Volviendo al fragmento de código, estamos analizando:

type(lambda: 0)(type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b''),{}
)(Exception())

Llamamos al constructor del objeto de código:

type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b'')

Pasamos el objeto de código y un diccionario vacío al constructor de un objeto de función:

type(lambda: 0)(type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b''),{}
)

Llamemos a la ayuda sobre un objeto de función para ver qué significan los argumentos.

Help on class function in module builtins:

class function(object)
 |  function(code, globals, name=None, argdefs=None, closure=None)
 |  
 |  Create a function object.
 |  
 |  code
 |    a code object
 |  globals
 |    the globals dictionary
 |  name
 |    a string that overrides the name from the code object
 |  argdefs
 |    a tuple that specifies the default argument values
 |  closure
 |    a tuple that supplies the bindings for free variables

Luego llamamos a la función construida pasando una instancia de Excepción como argumento. En consecuencia, llamamos a una función lambda que genera una excepción. Ejecutemos el fragmento y veamos que realmente funciona según lo previsto.

>>> type(lambda: 0)(type((lambda: 0).__code__)(
...     1,0,1,1,67,b'|\0\202\1\0',(),(),('x',),'','',1,b''),{}
... )(Exception())
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "", line 1, in 
Exception

Mejoras

Vimos que el último byte del bytecode es inútil. No abarrotemos esta complicada expresión innecesariamente. Eliminemos ese byte. Además, si queremos jugar un poco al golf, podríamos omitir la creación de instancias de Exception y, en su lugar, pasar la clase Exception como argumento. Esos cambios darían como resultado el siguiente código:

type(lambda: 0)(type((lambda: 0).__code__)(
    1,0,1,1,67,b'|\0\202\1',(),(),('x',),'','',1,b''),{}
)(Exception)

Cuando lo ejecutamos obtendremos el mismo resultado que antes. Es solo más corto.

katsu
fuente