Definieren Sie einen Lambda-Ausdruck, der eine Ausnahme auslöst

137

Wie kann ich einen Lambda-Ausdruck schreiben, der entspricht:

def x():
    raise Exception()

Folgendes ist nicht erlaubt:

y = lambda : raise Exception()
Thomas Jung
quelle
2
Das kannst du also nicht machen. Verwenden Sie normale Funktionen.
DrTyrsa
1
Was bringt es, einer anonymen Funktion einen Namen zu geben?
John La Rooy
2
@gnibbler Sie können den Namen verwenden, um auf die Funktion zu verweisen. y () ist in der REPL einfacher zu verwenden als (lambda: 0) ().
Thomas Jung
Also , was ist der Vorteil von y=lambda...über def y:dann?
John La Rooy
@gnibbler Irgendein Kontext: Ich wollte eine Funktion def g (f, e) definieren, die im glücklichen Fall f und e aufruft, wenn ein Fehler erkannt wurde. Abhängig vom Szenario kann e eine Ausnahme auslösen oder einen gültigen Wert zurückgeben. Um g zu verwenden, wollte ich g (Lambda x: x * 2, Lambda e: erhöhen e) oder alternativ g (Lambda x: x * 2, Lambda e: 0) schreiben.
Thomas Jung

Antworten:

169

Es gibt mehrere Möglichkeiten, einen Python zu häuten:

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

Lambdas akzeptieren Aussagen. Da raise exes sich um eine Aussage handelt, können Sie einen Allzweck-Raiser schreiben:

def raise_(ex):
    raise ex

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

Aber wenn Ihr Ziel darin besteht, a zu vermeiden def, schneidet dies offensichtlich nicht. Sie können jedoch Ausnahmen bedingt auslösen, z.

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

Alternativ können Sie eine Ausnahme auslösen, ohne eine benannte Funktion zu definieren. Alles was Sie brauchen ist ein starker Magen (und 2.x für den angegebenen Code):

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

Und eine starke Python3-Magenlösung :

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

Vielen Dank an @WarrenSpencer für den Hinweis auf eine sehr einfache Antwort, wenn es Ihnen egal ist, welche Ausnahme ausgelöst wird : y = lambda: 1/0.

Marcelo Cantos
quelle
117
OMG was für eine dunkle Kunst ist das?
CodeColorist
11
Wenn es Ihnen egal ist, welche Art von Ausnahme ausgelöst wird, funktioniert auch Folgendes : lambda: 1 / 0. Am Ende wird nur ein ZeroDivisionError anstelle einer regulären Ausnahme ausgelöst. Denken Sie daran, dass es für jemanden, der Ihren Code debuggt, seltsam aussehen kann, wenn sich die Ausnahme ausbreiten darf, um eine Reihe von ZeroDivisionErrors zu sehen.
Warren Spencer
Tolle Lösung @WarrenSpencer. Der meiste Code weist nicht viele Nullteilungsfehler auf, daher ist er so charakteristisch, als könnten Sie den Typ selbst auswählen.
JWG
2
y = 1/0ist eine super kluge Lösung, wenn der Ausnahmetyp irrelevant ist
Saher Ahwal
3
Kann uns jemand erklären, was in den Lösungen für „dunkle Kunst / starker Magen“ tatsächlich vor sich geht?
devalvaliert
56

Wie wäre es mit:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
quelle
12
Es ist ziemlich hackig, aber für das Schreiben von Tests, bei denen Sie Funktionen verspotten möchten, funktioniert dies ordentlich !!!
Kannan Ekanath
8
Funktioniert, aber Sie sollten es nicht tun.
August
1
Das funktioniert bei mir nicht, ich bekomme eine SyntaxErrorauf Python 2.7.11.
Nick Sweeting
Ich erhalte auch den obigen Fehler (SyntaxError) auf Python 2.7.5
Dinesh
1
Dies ist spezifisch für Python 3, aber ich glaube nicht, dass Python 2 dies zulässt.
Saher Ahwal
16

Eigentlich gibt es einen Weg, aber er ist sehr erfunden.

Mit der compile()integrierten Funktion können Sie ein Codeobjekt erstellen . Auf diese Weise können Sie die raiseAnweisung (oder eine andere Anweisung) verwenden, dies wirft jedoch eine weitere Herausforderung auf: die Ausführung des Codeobjekts. Der übliche Weg wäre, die execAnweisung zu verwenden, aber das führt Sie zurück zum ursprünglichen Problem, nämlich dass Sie keine Anweisungen in einem lambda(oder einem eval()) ausführen können .

Die Lösung ist ein Hack. Callables wie das Ergebnis einer lambdaAnweisung haben alle ein Attribut __code__, das tatsächlich ersetzt werden kann. Wenn Sie also ein aufrufbares __code__Objekt erstellen und dessen Wert durch das Codeobjekt von oben ersetzen , erhalten Sie etwas, das ohne Verwendung von Anweisungen ausgewertet werden kann. Das Erreichen all dessen führt jedoch zu einem sehr undurchsichtigen Code:

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

Das Obige bewirkt Folgendes:

  • Der compile()Aufruf erstellt ein Codeobjekt, das die Ausnahme auslöst.

  • Das lambda: 0gibt einen Aufruf zurück, der nichts anderes tut, als den Wert 0 zurückzugeben. Dies wird verwendet, um das obige Codeobjekt später auszuführen.

  • Das lambda x, y, zerstellt eine Funktion, die die __setattr__Methode des ersten Arguments mit den verbleibenden Argumenten aufruft und das erste Argument zurückgibt! Dies ist notwendig, weil es __setattr__selbst zurückkehrt None;

  • Der map()Aufruf nimmt das Ergebnis von lambda: 0und lambda x, y, zersetzt das __code__Objekt durch das Ergebnis des compile()Aufrufs. Das Ergebnis dieser Kartenoperation ist eine Liste mit einem Eintrag, der von zurückgegeben lambda x, y, zwird. Deshalb benötigen wir diesen lambda: Wenn wir ihn __setattr__sofort verwenden würden, würden wir den Verweis auf das lambda: 0Objekt verlieren !

  • Schließlich wird das erste (und einzige) Element der vom map()Aufruf zurückgegebenen Liste ausgeführt, was dazu führt, dass das Codeobjekt aufgerufen wird, wodurch letztendlich die gewünschte Ausnahme ausgelöst wird.

Es funktioniert (getestet in Python 2.6), aber es ist definitiv nicht schön.

Ein letzter Hinweis: Wenn Sie Zugriff auf das typesModul haben (für das die importAnweisung vor Ihrem verwendet werden müsste eval), können Sie diesen Code etwas verkürzen: Mit können types.FunctionType()Sie eine Funktion erstellen, die das angegebene Codeobjekt ausführt, sodass Sie gewonnen haben Es ist nicht nötig, eine Dummy-Funktion zu erstellen lambda: 0und den Wert ihres __code__Attributs zu ersetzen .

Michael Scarpa
quelle
15

Wenn Sie nur einen Lambda-Ausdruck wünschen, der eine beliebige Ausnahme auslöst, können Sie dies mit einem unzulässigen Ausdruck erreichen. Es wird beispielsweise lambda x: [][0]versucht, auf das erste Element in einer leeren Liste zuzugreifen, wodurch ein IndexError ausgelöst wird.

BITTE BEACHTEN SIE : Dies ist ein Hack, keine Funktion. Sie nicht verwenden , diese in jedem (nicht Code-Golf) Code , dass ein anderer Mensch sehen oder verwenden können.

Kyle Strand
quelle
In meinem Fall bekomme ich : TypeError: <lambda>() takes exactly 1 positional argument (2 given). Sind Sie sich des IndexError sicher?
Jovik
4
Ja. Haben Sie vielleicht die falsche Anzahl von Argumenten angegeben? Wenn Sie eine Lambda-Funktion benötigen, die eine beliebige Anzahl von Argumenten annehmen kann, verwenden Sie lambda *x: [][0]. (Die Originalversion verwendet nur ein Argument; für keine Argumente verwenden lambda : [][0]; für zwei verwenden lambda x,y: [][0]; usw.)
Kyle Strand
3
Ich habe dies ein wenig erweitert: lambda x: {}["I want to show this message. Called with: %s" % x] Produziert: KeyError: 'I want to show this message. Called with: foo'
ErlVolton
@ErlVolton Clever! Obwohl es eine schreckliche Idee ist, dies überall außer in einem einmaligen Skript zu verwenden ...
Kyle Strand
Ich verwende vorübergehend Unit-Tests für ein Projekt, bei dem ich mich nicht darum gekümmert habe, meinen Logger wirklich zu verspotten. Es wird ausgelöst, wenn Sie versuchen, einen Fehler oder einen kritischen Fehler zu protokollieren. Also ... ja schrecklich, obwohl einvernehmlich :)
ErlVolton
10

Ich möchte das UPDATE 3 der Antwort von Marcelo Cantos erläutern :

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

Erläuterung

lambda: 0 ist eine Instanz der builtins.function Klasse.
type(lambda: 0)ist die builtins.functionKlasse.
(lambda: 0).__code__ist ein codeObjekt.
Ein codeObjekt ist ein Objekt, das unter anderem den kompilierten Bytecode enthält. Es ist hier in CPython https://github.com/python/cpython/blob/master/Include/code.h definiert . Die Methoden werden hier implementiert: https://github.com/python/cpython/blob/master/Objects/codeobject.c . Wir können die Hilfe für das Codeobjekt ausführen:

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__)ist die Codeklasse.
Also wenn wir sagen

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

Wir rufen den Konstruktor des Codeobjekts mit den folgenden Argumenten auf:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • Stapelgröße = 1
  • Flags = 67
  • codestring = b '| \ 0 \ 202 \ 1 \ 0'
  • Konstanten = ()
  • names = ()
  • varnames = ('x',)
  • Dateiname = ''
  • name = ''
  • firstlineno = 1
  • lnotab = b ''

Informationen zur Bedeutung der Argumente finden Sie in der Definition von PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . Der Wert 67 für das flagsArgument ist beispielsweiseCO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE .

Das wichtigste Argument ist das, codestringdas Anweisungs-Opcodes enthält. Mal sehen, was sie bedeuten.

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

Die Dokumentation der Opcodes finden Sie hier https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . Das erste Byte ist der Opcode für LOAD_FAST, das zweite Byte ist sein Argument, dh 0.

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

Also schieben wir den Verweis auf xauf den Stapel. Das varnamesist eine Liste von Zeichenfolgen, die nur 'x' enthalten. Wir werden das einzige Argument der Funktion, die wir definieren, auf den Stapel übertragen.

Das nächste Byte ist der Opcode für RAISE_VARARGSund das nächste Byte ist sein Argument, dh 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)

Die AGB sind die Top-of-Stack. Da wir das erste Argument ( x) unserer Funktion in den Stapel verschoben haben und argc1 sind, werden wir das auslösen, xwenn es sich um eine Ausnahmeinstanz handelt, oder eine Instanz von xerstellen und es anderweitig auslösen.

Das letzte Byte, dh 0, wird nicht verwendet. Es ist kein gültiger Opcode. Es könnte genauso gut nicht da sein.

Zurück zum Code-Snippet:

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

Wir haben den Konstruktor des Codeobjekts aufgerufen:

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

Wir übergeben das Codeobjekt und ein leeres Wörterbuch an den Konstruktor eines Funktionsobjekts:

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

Rufen Sie die Hilfe für ein Funktionsobjekt auf, um zu sehen, was die Argumente bedeuten.

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

Wir rufen dann die konstruierte Funktion auf, die eine Exception-Instanz als Argument übergibt. Folglich haben wir eine Lambda-Funktion aufgerufen, die eine Ausnahme auslöst. Lassen Sie uns das Snippet ausführen und sehen, dass es tatsächlich wie beabsichtigt funktioniert.

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

Verbesserungen

Wir haben gesehen, dass das letzte Byte des Bytecodes nutzlos ist. Lassen Sie uns diesen komplizierten Ausdruck nicht nadeln. Lassen Sie uns dieses Byte entfernen. Auch wenn wir ein wenig Golf spielen möchten, können wir die Instanziierung von Exception weglassen und stattdessen die Exception-Klasse als Argument übergeben. Diese Änderungen würden zu folgendem Code führen:

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

Wenn wir es ausführen, erhalten wir das gleiche Ergebnis wie zuvor. Es ist nur kürzer.

Katsu
quelle