Tentukan ekspresi lambda yang memunculkan Exception

144

Bagaimana saya bisa menulis ekspresi lambda yang setara dengan:

def x():
    raise Exception()

Hal berikut ini tidak diperbolehkan:

y = lambda : raise Exception()
Thomas Jung
sumber
3
Jadi Anda tidak bisa melakukan itu. Gunakan fungsi normal.
DrTyrsa
1
Apa gunanya memberi nama pada fungsi anonim?
John La Rooy
2
@gnibbler Anda dapat menggunakan nama untuk merujuk ke fungsi tersebut. y () lebih mudah digunakan daripada (lambda: 0) () di REPL.
Thomas Jung
Jadi apa keuntungan dari y=lambda...over def y:itu?
John La Rooy
@gnibbler Beberapa konteks: Saya ingin mendefinisikan fungsi def g (f, e) yang memanggil f dalam happy case dan e jika kesalahan terdeteksi. Bergantung pada skenario, e dapat memunculkan pengecualian atau mengembalikan beberapa nilai yang valid. Untuk menggunakan g saya ingin menulis g (lambda x: x * 2, lambda e: raise e) atau alternatif g (lambda x: x * 2, lambda e: 0).
Thomas Jung

Jawaban:

173

Ada lebih dari satu cara untuk menguliti Python:

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

Lambdas menerima pernyataan. Karena raise exmerupakan pernyataan, Anda dapat menulis penggalang tujuan umum:

def raise_(ex):
    raise ex

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

Tetapi jika tujuan Anda adalah untuk menghindari a def, ini jelas tidak cukup. Itu memang, namun memungkinkan Anda untuk meningkatkan pengecualian secara bersyarat, misalnya:

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

Alternatifnya Anda bisa memunculkan pengecualian tanpa menentukan fungsi bernama. Yang Anda butuhkan hanyalah perut yang kuat (dan 2.x untuk kode yang diberikan):

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

Dan solusi perut kuat python3 :

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

Terima kasih @WarrenSpencer untuk menunjukkan jawaban yang sangat sederhana jika Anda tidak peduli yang eksepsi dimunculkan: y = lambda: 1/0.

Marcelo Cantos
sumber
127
OMG apa itu seni gelap?
CodeColorist
14
Jika Anda tidak peduli apa jenis eksepsi dilemparkan, berikut juga bekerja: lambda: 1 / 0. Anda hanya akan berakhir dengan ZeroDivisionError yang dilemparkan, bukan pengecualian biasa. Ingatlah bahwa jika pengecualian diizinkan untuk menyebar, mungkin terlihat aneh bagi seseorang yang men-debug kode Anda untuk mulai melihat sekelompok ZeroDivisionErrors.
Warren Spencer
Solusi hebat @WarrenSpencer. Sebagian besar kode tidak memiliki banyak kesalahan pembagian nol, jadi sangat berbeda jika Anda dapat memilih jenisnya sendiri.
jwg
2
y = 1/0adalah solusi super cerdas jika jenis Pengecualian tidak relevan
Saher Ahwal
5
Adakah yang bisa memberi tahu kami tentang apa yang sebenarnya terjadi dalam solusi 'seni gelap / perut kuat'?
Decvalts
60

Bagaimana tentang:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
sumber
12
Ini cukup hacky tetapi untuk tes menulis di mana Anda ingin fungsi tiruan ini berfungsi dengan baik !!!
Kannan Ekanath
10
Berhasil tetapi Anda tidak boleh melakukannya.
pelantikan
1
Ini tidak berhasil untuk saya, saya menggunakan SyntaxErrorPython 2.7.11.
Nick Sweeting
Saya juga mendapatkan kesalahan di atas (SyntaxError) pada Python 2.7.5
Dinesh
1
ini khusus untuk python 3 namun menurut saya python 2 tidak mengizinkannya.
Saher Ahwal
16

Sebenarnya ada caranya, tapi sangat dibuat-buat.

Anda dapat membuat objek kode menggunakan compile()fungsi bawaan. Ini memungkinkan Anda untuk menggunakan raisepernyataan (atau pernyataan lain, dalam hal ini), tetapi ini menimbulkan tantangan lain: mengeksekusi objek kode. Cara biasa adalah dengan menggunakan execpernyataan, tetapi itu membawa Anda kembali ke masalah aslinya, yaitu Anda tidak dapat mengeksekusi pernyataan dalam lambda(atau eval(), dalam hal ini).

Solusinya adalah hack. Callable seperti hasil dari sebuah lambdapernyataan semuanya memiliki atribut __code__, yang sebenarnya dapat diganti. Jadi, jika Anda membuat callable dan mengganti __code__nilainya dengan objek kode dari atas, Anda mendapatkan sesuatu yang dapat dievaluasi tanpa menggunakan pernyataan. Namun, pencapaian semua ini menghasilkan kode yang sangat tidak jelas:

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

Di atas melakukan hal berikut:

  • yang compile()panggilan membuat objek kode yang menimbulkan pengecualian;

  • yang lambda: 0kembali sebuah callable yang tidak hanya mengembalikan nilai 0 - ini digunakan untuk mengeksekusi obyek kode di atas kemudian;

  • yang lambda x, y, zmembuat fungsi yang memanggil __setattr__metode argumen pertama dengan argumen yang tersisa, DAN MENGEMBALIKAN ARGUMEN PERTAMA! Ini perlu, karena dengan __setattr__sendirinya kembali None;

  • yang map()panggilan mengambil hasil lambda: 0, dan menggunakan lambda x, y, zmenggantikannya ini __code__objek dengan hasil dari compile()panggilan. Hasil dari operasi peta ini adalah daftar dengan satu entri, yang dikembalikan oleh lambda x, y, z, itulah mengapa kita membutuhkan ini lambda: jika kita akan __setattr__langsung menggunakannya , kita akan kehilangan referensi ke lambda: 0objek!

  • akhirnya, elemen pertama (dan satu-satunya) dari daftar yang dikembalikan oleh map()panggilan tersebut dijalankan, menghasilkan objek kode yang dipanggil, yang pada akhirnya meningkatkan pengecualian yang diinginkan.

Ini berfungsi (diuji dengan Python 2.6), tetapi jelas tidak cantik.

Satu catatan terakhir: jika Anda memiliki akses ke typesmodul (yang akan membutuhkan untuk menggunakan importpernyataan sebelum Anda eval), maka Anda dapat mempersingkat kode ini sedikit: menggunakan types.FunctionType()Anda dapat membuat fungsi yang akan mengeksekusi objek kode yang diberikan, jadi Anda menang tidak perlu retasan untuk membuat fungsi dummy dengan lambda: 0dan mengganti nilai __code__atributnya.

Michael Scarpa
sumber
15

Jika yang Anda inginkan hanyalah ekspresi lambda yang memunculkan pengecualian sewenang-wenang, Anda dapat melakukannya dengan ekspresi ilegal. Misalnya, lambda x: [][0]akan mencoba mengakses elemen pertama dalam daftar kosong, yang akan memunculkan IndexError.

HARAP DICATAT : Ini adalah hack, bukan fitur. Jangan gunakan ini dalam kode (non-kode-golf) yang mungkin dilihat atau digunakan oleh orang lain.

Kyle Strand
sumber
Dalam kasus saya saya mendapatkan: TypeError: <lambda>() takes exactly 1 positional argument (2 given). Apakah Anda yakin dengan IndexError?
Jovik
4
Ya. Apakah Anda mungkin memberikan jumlah argumen yang salah? Jika Anda membutuhkan fungsi lambda yang dapat menerima sejumlah argumen, gunakan lambda *x: [][0]. (Versi asli hanya membutuhkan satu argumen; tanpa argumen, gunakan lambda : [][0]; untuk dua, gunakan lambda x,y: [][0]; dll.)
Kyle Strand
4
Saya telah mengembangkan ini sedikit: lambda x: {}["I want to show this message. Called with: %s" % x] Menghasilkan: KeyError: 'I want to show this message. Called with: foo'
ErlVolton
@ErlVolPintar! Meskipun menggunakan ini di mana saja kecuali dalam skrip satu kali sepertinya ide yang buruk ...
Kyle Strand
Saya menggunakan sementara dalam pengujian unit untuk proyek di mana saya tidak repot-repot membuat tiruan nyata dari logger saya. Ini muncul jika Anda mencoba untuk mencatat kesalahan atau kritis. Jadi ... Ya mengerikan, meskipun suka sama suka :)
ErlVolton
14

Saya ingin memberikan penjelasan UPDATE 3 dari jawaban yang diberikan oleh Marcelo Cantos:

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

Penjelasan

lambda: 0adalah turunan dari builtins.functionkelas.
type(lambda: 0)adalah builtins.functionkelasnya.
(lambda: 0).__code__adalah sebuah codeobjek.
Sebuah codeobjek adalah obyek yang memegang bytecode disusun antara lain. Ini didefinisikan di sini di CPython https://github.com/python/cpython/blob/master/Include/code.h . Metodenya diterapkan di sini https://github.com/python/cpython/blob/master/Objects/codeobject.c . Kami dapat menjalankan bantuan pada objek kode:

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__)adalah kelas kode.
Jadi saat kita bilang

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

kami memanggil konstruktor objek kode dengan argumen berikut:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • stacksize = 1
  • bendera = 67
  • codestring = b '| \ 0 \ 202 \ 1 \ 0'
  • konstanta = ()
  • nama = ()
  • varnames = ('x',)
  • nama file = ''
  • nama = ''
  • firstlineno = 1
  • lnotab = b ''

Anda dapat membaca tentang apa arti argumen dalam definisi PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . Nilai 67 untuk flagsargumen sebagai contoh CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE.

Argumen yang paling penting adalah codestringyang berisi opcode instruksi. Mari kita lihat apa artinya.

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

Dokumentasi opcode dapat ditemukan di sini https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . Byte pertama adalah opcode untuk LOAD_FAST, byte kedua adalah argumennya yaitu 0.

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

Jadi kami mendorong referensi ke xke tumpukan. Ini varnamesadalah daftar string yang hanya berisi 'x'. Kami akan mendorong satu-satunya argumen dari fungsi yang kami definisikan ke tumpukan.

Byte berikutnya adalah opcode untuk RAISE_VARARGSdan byte berikutnya adalah argumennya yaitu 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 adalah top-of-stack. Karena kita mendorong argumen pertama ( x) dari fungsi kita ke tumpukan dan argcbernilai 1, kita akan menaikkan xjika itu adalah contoh pengecualian atau membuat contoh xdan menaikkannya sebaliknya.

Byte terakhir yaitu 0 tidak digunakan. Ini bukan opcode yang valid. Mungkin juga tidak ada di sana.

Kembali ke cuplikan kode yang kami analisis:

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

Kami memanggil konstruktor objek kode:

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

Kami meneruskan objek kode dan kamus kosong ke konstruktor objek fungsi:

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

Mari panggil bantuan pada objek fungsi untuk melihat apa arti argumen tersebut.

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

Kami kemudian memanggil fungsi yang dibangun dengan meneruskan instance Exception sebagai argumen. Akibatnya kami memanggil fungsi lambda yang memunculkan pengecualian. Mari kita jalankan cuplikannya dan lihat apakah itu berfungsi sebagaimana mestinya.

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

Perbaikan

Kami melihat bahwa byte terakhir dari bytecode tidak berguna. Jangan mengacaukan ekspresi rumit ini dengan sia-sia. Mari hapus byte itu. Juga jika kita ingin bermain golf sedikit, kita bisa menghilangkan instansiasi Exception dan sebagai gantinya meneruskan kelas Exception sebagai argumen. Perubahan tersebut akan menghasilkan kode berikut:

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

Ketika kita menjalankannya kita akan mendapatkan hasil yang sama seperti sebelumnya. Itu hanya lebih pendek.

katsu
sumber