Zdefiniuj wyrażenie lambda, które wywołuje wyjątek

137

Jak napisać wyrażenie lambda, które jest równoważne z:

def x():
    raise Exception()

Niedozwolone jest:

y = lambda : raise Exception()
Thomas Jung
źródło
2
Więc nie możesz tego zrobić. Używaj normalnych funkcji.
DrTyrsa
1
Jaki jest sens nadawania nazwy anonimowej funkcji?
John La Rooy
2
@gnibbler Możesz użyć nazwy w celu odniesienia się do funkcji. y () jest łatwiejsze w użyciu niż (lambda: 0) () w REPL.
Thomas Jung
Więc jaka jest przewaga y=lambda...nad def y:tym?
John La Rooy
@gnibbler Pewien kontekst: Chciałem zdefiniować funkcję def g (f, e), która wywołuje f w szczęśliwym przypadku ie, jeśli zostanie wykryty błąd. W zależności od scenariusza e może zgłosić wyjątek lub zwrócić jakąś prawidłową wartość. Aby użyć g, chciałem napisać g (lambda x: x * 2, lambda e: podbić e) lub alternatywnie g (lambda x: x * 2, lambda e: 0).
Thomas Jung

Odpowiedzi:

169

Istnieje więcej niż jeden sposób na skórowanie Pythona:

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

Lambdy przyjmują oświadczenia. Ponieważ raise exjest to stwierdzenie, możesz napisać podniesienie ogólnego przeznaczenia:

def raise_(ex):
    raise ex

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

Ale jeśli Twoim celem jest uniknięcie a def, to oczywiście nie wystarczy. Umożliwia jednak warunkowe zgłaszanie wyjątków, np .:

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

Alternatywnie możesz zgłosić wyjątek bez definiowania nazwanej funkcji. Wystarczy mocny żołądek (i 2.x dla podanego kodu):

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

I mocne rozwiązanie żołądkowe Python3 :

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

Dzięki @WarrenSpencer za wskazanie bardzo prostą odpowiedź, jeśli nie obchodzi który jest wyjątek: y = lambda: 1/0.

Marcelo Cantos
źródło
117
OMG, co to za mroczna sztuka?
CodeColorist
11
Jeśli nie obchodzi, jakiego rodzaju jest wyjątek, następujące prace również: lambda: 1 / 0. Po prostu skończysz z wyrzuceniem ZeroDivisionError zamiast zwykłego wyjątku. Należy pamiętać, że jeśli wyjątek może się rozprzestrzeniać, ktoś debugujący kod może wyglądać dziwnie, aby zobaczyć kilka błędów ZeroDivisionErrors.
Warren Spencer
Świetne rozwiązanie @WarrenSpencer. Większość kodu nie ma wielu błędów dzielenia zerowego, więc jest tak charakterystyczny, jakbyś mógł sam wybrać typ.
jwg,
2
y = 1/0jest super inteligentnym rozwiązaniem, jeśli typ wyjątku jest nieistotny
Saher Ahwal
3
Czy ktoś może nas opowiedzieć, co tak naprawdę dzieje się w rozwiązaniach „mrocznej sztuki / silnego żołądka”?
decvalts
56

Co powiesz na:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
źródło
12
Jest to dość hakerskie, ale w przypadku pisania testów, w których chcesz mockować funkcje, działa to dobrze !!!
Kannan Ekanath
8
Działa, ale nie powinieneś tego robić.
augurar
1
To nie działa dla mnie, dostaję SyntaxErrorna Python 2.7.11.
Nick Sweeting
Otrzymuję również powyższy błąd (SyntaxError) w Pythonie 2.7.5
Dinesh
1
jest to specyficzne dla Pythona 3, ale nie sądzę, że Python 2 na to pozwala.
Saher Ahwal
16

Właściwie jest sposób, ale jest bardzo wymyślony.

Możesz utworzyć obiekt kodu za pomocą funkcji compile()wbudowanej. Pozwala to na użycie raiseinstrukcji (lub dowolnej innej instrukcji), ale rodzi inne wyzwanie: wykonanie obiektu kodu. Zwykłym sposobem byłoby użycie execinstrukcji, ale to prowadzi cię z powrotem do pierwotnego problemu, a mianowicie, że nie możesz wykonać instrukcji w a lambda(lub eval(), jeśli o to chodzi).

Rozwiązaniem jest włamanie. Wszystkie wywołania, takie jak wynik lambdainstrukcji, mają atrybut __code__, który w rzeczywistości można zastąpić. Tak więc, jeśli utworzysz wywoływalny i zastąpisz jego __code__wartość obiektem kodu z góry, otrzymasz coś, co można ocenić bez użycia instrukcji. Jednak osiągnięcie tego wszystkiego skutkuje bardzo niejasnym kodem:

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

Powyższe wykonuje następujące czynności:

  • compile()wezwanie tworzy obiekt kodu, który podnosi wyjątek;

  • gdy lambda: 0wraca wywoływalnym że nic nie robi, ale zwraca wartość 0 - to jest wykorzystywane do wykonywania powyższego celu kodu później;

  • lambda x, y, ztworzy funkcję, która wywołuje __setattr__metodę pierwszego argumentu z pozostałych argumentów i zwraca pierwszy argument! Jest to konieczne, ponieważ __setattr__samo wraca None;

  • map()rozmowa odbywa wynik lambda: 0, a przy użyciu lambda x, y, zzastępuje go za __code__obiekt o wyniku compile()rozmowy. Rezultatem tej operacji na mapie jest lista z jednym wpisem, tym zwróconym przez lambda x, y, z, dlatego potrzebujemy tego lambda: gdybyśmy użyli __setattr__od razu, stracilibyśmy odniesienie do lambda: 0obiektu!

  • na koniec wykonywany jest pierwszy (i jedyny) element listy zwracany przez map()wywołanie, co powoduje wywołanie obiektu kodu, ostatecznie wywołując żądany wyjątek.

Działa (przetestowany w Pythonie 2.6), ale zdecydowanie nie jest ładny.

Ostatnia uwaga: jeśli masz dostęp do typesmodułu (co wymagałoby użycia importinstrukcji przed Twoim eval), to możesz nieco skrócić ten kod: używając types.FunctionType()możesz stworzyć funkcję, która wykona dany obiekt kodu, więc wygrałeś Nie potrzebuję hackowania tworzenia funkcji fikcyjnej zi lambda: 0zastępowania wartości jej __code__atrybutu.

Michael Scarpa
źródło
15

Jeśli wszystko, czego chcesz, to wyrażenie lambda, które wywołuje dowolny wyjątek, możesz to zrobić za pomocą niedozwolonego wyrażenia. Na przykład lambda x: [][0]spróbuje uzyskać dostęp do pierwszego elementu z pustej listy, co spowoduje zgłoszenie błędu IndexError.

UWAGA : To hack, a nie funkcja. Nie używaj tego w żadnym (innym niż code-golf) kodzie, który może zobaczyć lub użyć inny człowiek.

Kyle Strand
źródło
W moim przypadku otrzymujemy: TypeError: <lambda>() takes exactly 1 positional argument (2 given). Czy na pewno wystąpił błąd IndexError?
Jovik,
4
Tak. Czy może podałeś niewłaściwą liczbę argumentów? Jeśli potrzebujesz funkcji lambda, która może przyjmować dowolną liczbę argumentów, użyj lambda *x: [][0]. (Oryginalna wersja przyjmuje tylko jeden argument; bez argumentów użyj lambda : [][0]; dla dwóch użyj lambda x,y: [][0]; itp.)
Kyle Strand
3
Trochę to rozszerzyłem: lambda x: {}["I want to show this message. Called with: %s" % x] Produkuje: KeyError: 'I want to show this message. Called with: foo'
ErlVolton,
@ErlVolton Clever! Chociaż używanie tego wszędzie, z wyjątkiem jednorazowego skryptu, wydaje się okropnym pomysłem ...
Kyle Strand
Tymczasowo używam testów jednostkowych dla projektu, w którym nie zadałem sobie trudu, aby zrobić prawdziwą próbę mojego rejestratora. Podnosi się, jeśli spróbujesz zarejestrować błąd lub krytyczny. A więc ... Tak okropne, chociaż za zgodą :)
ErlVolton
10

Chciałbym wyjaśnić AKTUALIZACJĘ 3 odpowiedzi udzielonej przez Marcelo Cantos:

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

Wyjaśnienie

lambda: 0jest instancją builtins.functionklasy.
type(lambda: 0)jest builtins.functionklasą.
(lambda: 0).__code__jest codeprzedmiotem. Obiekt jest obiektem, który posiada skompilowany kod bajtowy między innymi. Jest zdefiniowany tutaj w CPython https://github.com/python/cpython/blob/master/Include/code.h . Jego metody są zaimplementowane tutaj https://github.com/python/cpython/blob/master/Objects/codeobject.c . Możemy uruchomić pomoc dotyczącą obiektu kodu:
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__)to klasa kodu.
Więc kiedy mówimy

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

wywołujemy konstruktora obiektu kodu z następującymi argumentami:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • stacksize = 1
  • flagi = 67
  • codestring = b '| \ 0 \ 202 \ 1 \ 0'
  • stałe = ()
  • nazwy = ()
  • varnames = ('x',)
  • filename = ''
  • name = ''
  • firstlineno = 1
  • lnotab = b ''

O tym, co oznaczają argumenty, możesz przeczytać w definicji PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . Na przykład wartość 67 flagsargumentu to CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE.

Najważniejszym argumentem jest ten, codestringktóry zawiera kody instrukcji. Zobaczmy, co mają na myśli.

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

Dokumentację kodów operacyjnych można znaleźć tutaj https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . Pierwszy bajt to kod LOAD_FASToperacji, drugi bajt to jego argument, tj. 0.

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

Więc umieszczamy odniesienie do xna stosie. varnamesJest listą ciągów zawierających tylko „x”. Jedyny argument funkcji, którą definiujemy, wrzucimy na stos.

Następny bajt to kod operacji dla, RAISE_VARARGSa następny bajt to jego argument, tj. 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)

TOS jest szczytem stosu. Ponieważ umieściliśmy pierwszy argument ( x) naszej funkcji na stosie i argcwynosi 1, podniesiemy wartość, xjeśli jest to wyjątek, lub utworzymy instancję xi podniesiemy ją w przeciwnym razie.

Ostatni bajt, czyli 0, nie jest używany. To nie jest prawidłowy kod operacji. Równie dobrze może go tam nie być.

Wracając do fragmentu kodu, który teraz analizujemy:

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

Nazwaliśmy konstruktora obiektu kodu:

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

Przekazujemy obiekt kodu i pusty słownik do konstruktora obiektu funkcji:

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

Wywołajmy pomoc dotyczącą obiektu funkcji, aby zobaczyć, co oznaczają argumenty.

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

Następnie wywołujemy skonstruowaną funkcję, przekazując jako argument wystąpienie wyjątku. W konsekwencji nazwaliśmy funkcję lambda, która zgłasza wyjątek. Uruchommy fragment i przekonajmy się, że rzeczywiście działa zgodnie z przeznaczeniem.

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

Improvements

Widzieliśmy, że ostatni bajt kodu bajtowego jest bezużyteczny. Nie zaśmiecajmy tej skomplikowanej ekspresji igliwie. Usuńmy ten bajt. Również jeśli chcemy trochę pograć w golfa, możemy pominąć tworzenie wystąpienia Exception i zamiast tego przekazać klasę Exception jako argument. Te zmiany spowodowałyby następujący kod:

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

Kiedy go uruchomimy, uzyskamy taki sam wynik jak poprzednio. Jest po prostu krótszy.

katsu
źródło