Définir une expression lambda qui lève une exception

137

Comment puis-je écrire une expression lambda équivalente à:

def x():
    raise Exception()

Ce qui suit n'est pas autorisé:

y = lambda : raise Exception()
Thomas Jung
la source
2
Donc vous ne pouvez pas faire ça. Utilisez les fonctions normales.
DrTyrsa
1
Quel est l'intérêt de donner un nom à une fonction anonyme?
John La Rooy
2
@gnibbler Vous pouvez utiliser le nom pour faire référence à la fonction. y () est plus facile à utiliser que (lambda: 0) () dans le REPL.
Thomas Jung
Alors, quel est l'avantage de y=lambda...plus def y:alors?
John La Rooy
@gnibbler Un peu de contexte: je voulais définir une fonction def g (f, e) qui appelle f dans le cas heureux et e si une erreur était détectée. Selon le scénario, e peut déclencher une exception ou renvoyer une valeur valide. Pour utiliser g, je voulais écrire g (lambda x: x * 2, lambda e: lever e) ou encore g (lambda x: x * 2, lambda e: 0).
Thomas Jung

Réponses:

169

Il existe plus d'une façon de skin un Python:

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

Les lambdas acceptent les déclarations. Puisqu'il raise exs'agit d'une instruction, vous pouvez écrire un relanceur à usage général:

def raise_(ex):
    raise ex

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

Mais si votre objectif est d'éviter un def, cela ne le coupe évidemment pas. Il vous permet cependant de déclencher des exceptions conditionnelles, par exemple:

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

Vous pouvez également lever une exception sans définir de fonction nommée. Tout ce dont vous avez besoin est un estomac solide (et 2.x pour le code donné):

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

Et une solution forte pour l'estomac python3 :

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

Merci d'avoir signalé @WarrenSpencer une réponse très simple si vous ne vous inquiétez pas quelle exception est soulevée: y = lambda: 1/0.

Marcelo Cantos
la source
117
OMG quel art sombre c'est?
CodeColorist
11
Si vous ne vous souciez pas ce type d'exception est levée, ce qui suit fonctionne aussi: lambda: 1 / 0. Vous vous retrouverez simplement avec une erreur ZeroDivisionError au lieu d'une exception normale. Gardez à l'esprit que si l'exception est autorisée à se propager, il peut sembler étrange à quelqu'un de déboguer votre code de commencer à voir un tas de ZeroDivisionErrors.
Warren Spencer
Excellente solution @WarrenSpencer. La plupart du code n'a pas beaucoup d'erreurs de division zéro, il est donc aussi distinctif que si vous pouviez choisir le type vous-même.
jwg le
2
y = 1/0est super solution intelligente si le type d'exception est hors de propos
Saher Ahwal
3
Quelqu'un peut-il nous expliquer ce qui se passe réellement dans les solutions «art sombre / estomac fort»?
Décalte
56

Que diriez-vous:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
la source
12
C'est assez piraté mais pour écrire des tests où vous voulez vous moquer des fonctions cela fonctionne très bien !!!
Kannan Ekanath
8
Fonctionne mais vous ne devriez pas le faire.
août
1
Cela ne fonctionne pas pour moi, j'obtiens un SyntaxErrorsur Python 2.7.11.
Nick Sweeting
J'obtiens également l'erreur ci-dessus (SyntaxError) sur Python 2.7.5
Dinesh
1
c'est spécifique à python 3 mais je ne pense pas que python 2 le permette.
Saher Ahwal
16

En fait, il y a un moyen, mais c'est très artificiel.

Vous pouvez créer un objet code à l'aide de la compile()fonction intégrée. Cela vous permet d'utiliser l' raiseinstruction (ou toute autre instruction, d'ailleurs), mais cela soulève un autre défi: exécuter l'objet code. La manière habituelle serait d'utiliser l' execinstruction, mais cela vous ramène au problème d'origine, à savoir que vous ne pouvez pas exécuter des instructions dans un lambda(ou un eval(), d'ailleurs).

La solution est un hack. Les appelables comme le résultat d'une lambdainstruction ont tous un attribut __code__, qui peut en fait être remplacé. Ainsi, si vous créez un appelable et remplacez sa __code__valeur par l'objet de code ci-dessus, vous obtenez quelque chose qui peut être évalué sans utiliser d'instructions. Cependant, réaliser tout cela entraîne un code très obscur:

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

Ce qui précède fait ce qui suit:

  • l' compile()appel crée un objet code qui déclenche l'exception;

  • le lambda: 0retourne un appelable qui ne fait que renvoyer la valeur 0 - ceci est utilisé pour exécuter l'objet de code ci-dessus plus tard;

  • le lambda x, y, zcrée une fonction qui appelle la __setattr__méthode du premier argument avec les arguments restants, ET RENVOIE LE PREMIER ARGUMENT! Cela est nécessaire, car __setattr__lui - même revient None;

  • l' map()appel prend le résultat de lambda: 0, et l'utilisation de lambda x, y, zremplace son __code__objet par le résultat de l' compile()appel. Le résultat de cette opération de carte est une liste avec une entrée, celle renvoyée par lambda x, y, z, c'est pourquoi nous en avons besoin lambda: si nous l'utilisions __setattr__tout de suite, nous perdrions la référence à l' lambda: 0objet!

  • enfin, le premier (et unique) élément de la liste renvoyée par l' map()appel est exécuté, ce qui entraîne l' appel de l'objet code, levant finalement l'exception souhaitée.

Cela fonctionne (testé en Python 2.6), mais ce n'est certainement pas joli.

Une dernière remarque: si vous avez accès au typesmodule (ce qui nécessiterait d'utiliser l' importinstruction avant votre eval), vous pouvez raccourcir un peu ce code: en utilisant, types.FunctionType()vous pouvez créer une fonction qui exécutera l'objet de code donné, vous avez donc gagné 'pas besoin de créer une fonction factice avec lambda: 0et de remplacer la valeur de son __code__attribut.

Michael Scarpa
la source
15

Si tout ce que vous voulez est une expression lambda qui déclenche une exception arbitraire, vous pouvez le faire avec une expression illégale. Par exemple, lambda x: [][0]essaiera d'accéder au premier élément d'une liste vide, ce qui déclenchera une IndexError.

VEUILLEZ NOTER : Ceci est un hack, pas une fonctionnalité. Ne l' utilisez dans aucun code (non codé-golf) qu'un autre être humain pourrait voir ou utiliser.

Kyle Strand
la source
Dans mon cas , je reçois: TypeError: <lambda>() takes exactly 1 positional argument (2 given). Êtes-vous sûr de l'IndexError?
Jovik
4
Oui. Avez-vous peut-être fourni le mauvais nombre d'arguments? Si vous avez besoin d'une fonction lambda pouvant accepter n'importe quel nombre d'arguments, utilisez lambda *x: [][0]. (La version originale ne prend qu'un seul argument; pour aucun argument, utilisez lambda : [][0]; pour deux, utilisez lambda x,y: [][0]; etc.)
Kyle Strand
3
J'ai développé un peu ceci: lambda x: {}["I want to show this message. Called with: %s" % x] Produit: KeyError: 'I want to show this message. Called with: foo'
ErlVolton
@ErlVolton Clever! Bien que l'utiliser n'importe où sauf dans un script ponctuel semble être une idée terrible ...
Kyle Strand
J'utilise temporairement des tests unitaires pour un projet où je n'ai pas pris la peine de faire une vraie simulation de mon enregistreur. Il se déclenche si vous essayez de consigner une erreur ou une erreur critique. Alors ... Oui terrible, bien que consensuel :)
ErlVolton
10

J'aimerais donner une explication de la MISE À JOUR 3 de la réponse fournie par Marcelo Cantos:

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

Explication

lambda: 0est une instance de la builtins.functionclasse.
type(lambda: 0)est la builtins.functionclasse.
(lambda: 0).__code__est un codeobjet.
Un codeobjet est un objet qui contient le bytecode compilé entre autres. Il est défini ici en CPython https://github.com/python/cpython/blob/master/Include/code.h . Ses méthodes sont implémentées ici https://github.com/python/cpython/blob/master/Objects/codeobject.c . Nous pouvons exécuter l'aide sur l'objet code:

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__)est la classe de code.
Alors quand on dit

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

nous appelons le constructeur de l'objet code avec les arguments suivants:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • stacksize = 1
  • drapeaux = 67
  • codestring = b '| \ 0 \ 202 \ 1 \ 0'
  • constantes = ()
  • noms = ()
  • varnames = ('x',)
  • nom de fichier = ''
  • nom = ''
  • firstlineno = 1
  • lnotab = b ''

Vous pouvez lire ce que signifient les arguments dans la définition du PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . La valeur de 67 pour l' flagsargument est par exemple CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE.

L'argument le plus important est celui codestringqui contient les opcodes d'instructions. Voyons ce qu'ils signifient.

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

La documentation des opcodes peut être trouvée ici https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . Le premier octet est l'opcode pour LOAD_FAST, le second octet est son argument c'est à dire 0.

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

Nous poussons donc la référence xsur la pile. Le varnamesest une liste de chaînes contenant uniquement «x». Nous allons pousser le seul argument de la fonction que nous définissons dans la pile.

L'octet suivant est l'opcode pour RAISE_VARARGSet l'octet suivant est son argument, c'est-à-dire 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)

Le TOS est le top-of-stack. Puisque nous avons poussé le premier argument ( x) de notre fonction dans la pile et qu'il argcvaut 1, nous lèverons le xs'il s'agit d'une instance d'exception ou créerons une instance de xet la lèverons autrement.

Le dernier octet, c'est-à-dire 0, n'est pas utilisé. Ce n'est pas un opcode valide. Cela pourrait aussi bien ne pas être là.

Revenons à l'extrait de code que nous sommes en train de faire:

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

Nous avons appelé le constructeur de l'objet code:

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

On passe l'objet code et un dictionnaire vide au constructeur d'un objet fonction:

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

Appelons help sur un objet fonction pour voir ce que signifient les arguments.

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

Nous appelons ensuite la fonction construite en passant une instance d'Exception comme argument. Par conséquent, nous avons appelé une fonction lambda qui lève une exception. Exécutons l'extrait et voyons qu'il fonctionne effectivement comme prévu.

>>> 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

Améliorations

Nous avons vu que le dernier octet du bytecode est inutile. N'encombrons pas inutilement cette expression compliquée. Supprimons cet octet. Aussi, si nous voulons jouer un peu au golf, nous pourrions omettre l'instanciation d'Exception et passer à la place la classe Exception comme argument. Ces modifications entraîneraient le code suivant:

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

Lorsque nous l'exécutons, nous obtiendrons le même résultat qu'avant. C'est juste plus court.

katsu
la source