Definir uma expressão lambda que gera uma exceção


137

Como posso escrever uma expressão lambda equivalente a:

def x():
    raise Exception()

O seguinte não é permitido:

y = lambda : raise Exception()

2
Então você não pode fazer isso. Use funções normais.
DrTyrsa

1
Qual é o sentido de atribuir um nome a uma função anônima?
John La Rooy

2
@gnibbler Você pode usar o nome para se referir à função. y () é mais fácil de usar do que (lambda: 0) () no REPL.
Thomas Jung

Então, qual é a vantagem de y=lambda...mais def y:, então?
John La Rooy

@gnibbler Algum contexto: eu queria definir uma função def g (f, e) que chame f no caso feliz ee se um erro for detectado. Dependendo do cenário, é possível gerar uma exceção ou retornar algum valor válido. Para usar g, eu queria escrever g (lambda x: x * 2, lambda e: raise e) ou, alternativamente, g (lambda x: x * 2, lambda e: 0).
Thomas Jung

Respostas:


169

Há mais de uma maneira de esfolar um Python:

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

Lambdas aceitam declarações. Como raise exé uma declaração, você pode escrever um raiser de propósito geral:

def raise_(ex):
    raise ex

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

Mas se seu objetivo é evitar um def, obviamente isso não é suficiente. No entanto, permite gerar exceções condicionais, por exemplo:

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

Como alternativa, você pode gerar uma exceção sem definir uma função nomeada. Tudo o que você precisa é de um estômago forte (e 2.x para o código fornecido):

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

E uma solução forte para o estômago python3 :

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

Graças @WarrenSpencer por apontar uma resposta muito simples, se você não se importa que exceção é levantada: y = lambda: 1/0.


117
OMG que arte escura é?
precisa saber é o seguinte

11
Se você não se importa que tipo de exceção é lançada, o seguinte também funciona: lambda: 1 / 0. Você acabará lançando um ZeroDivisionError em vez de uma exceção regular. Lembre-se de que, se a exceção for permitida, pode parecer estranho alguém depurando seu código para começar a ver um monte de ZeroDivisionErrors.
Warren Spencer

Ótima solução @WarrenSpencer. A maioria dos códigos não possui muitos erros de divisão zero, por isso é tão distinto como se você pudesse escolher o tipo.
jwg 29/09/17

2
y = 1/0é solução inteligente super-se Tipo de exceção é irrelevante
Saher Ahwal

3
Alguém pode nos falar sobre o que realmente está acontecendo nas soluções de 'arte escura / estômago forte'?
decvalts

56

E se:

lambda x: exec('raise(Exception(x))')

12
É bastante hacky, mas para escrever testes nos quais você deseja zombar de funções, isso funciona perfeitamente !!!
Kannan Ekanath

8
Funciona, mas você não deve fazê-lo.
Augurar 17/04

1
Isso não funciona para mim, eu recebo um SyntaxErrorno Python 2.7.11.
22816 Nick Sweeting

Também estou recebendo o erro acima (SyntaxError) no Python 2.7.5
Dinesh

1
isso é específico do python 3, no entanto, não acho que o python 2 permita isso.
Saher Ahwal

16

Na verdade, existe um caminho, mas é muito artificial.

Você pode criar um objeto de código usando a compile()função interna. Isso permite que você use a raiseinstrução (ou qualquer outra instrução, nesse caso), mas levanta outro desafio: executar o objeto de código. A maneira usual seria usar a execinstrução, mas isso o levará de volta ao problema original, a saber, que você não pode executar instruções em um lambda(ou um eval(), nesse caso).

A solução é um hack. lambdaTodos os callables, como o resultado de uma instrução, têm um atributo __code__que pode ser substituído. Portanto, se você criar uma chamada e substituir seu __code__valor pelo objeto de código acima, obtém algo que pode ser avaliado sem o uso de instruções. Conseguir tudo isso, no entanto, resulta em código muito obscuro:

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

O acima faz o seguinte:

  • a compile()chamada cria um objeto de código que gera a exceção;

  • o lambda: 0retorna uma chamada que não faz nada além de retornar o valor 0 - isso é usado para executar o objeto de código acima posteriormente;

  • o lambda x, y, zcria uma função que chama o __setattr__método do primeiro argumento com os argumentos restantes e retorna o primeiro argumento! Isso é necessário, porque __setattr__ele retorna None;

  • a map()chamada obtém o resultado de lambda: 0, e usando o lambda x, y, zsubstitui seu __code__objeto pelo resultado da compile()chamada. O resultado dessa operação de mapa é uma lista com uma entrada, a retornada por lambda x, y, z, e é por isso que precisamos disso lambda: se __setattr__usássemos imediatamente, perderíamos a referência ao lambda: 0objeto!

  • finalmente, o primeiro (e único) elemento da lista retornado pela map()chamada é executado, resultando na chamada do objeto de código e, finalmente, gerando a exceção desejada.

Funciona (testado no Python 2.6), mas definitivamente não é bonito.

Uma última observação: se você tiver acesso ao typesmódulo (que exigiria usar a importinstrução antes da sua eval), poderá reduzir um pouco esse código: usando types.FunctionType()você pode criar uma função que executará o objeto de código fornecido, para ganhar é necessário o hack de criar uma função fictícia lambda: 0e substituir o valor de seu __code__atributo.



15

Se tudo o que você deseja é uma expressão lambda que gera uma exceção arbitrária, você pode fazer isso com uma expressão ilegal. Por exemplo, lambda x: [][0]tentará acessar o primeiro elemento em uma lista vazia, o que gerará um IndexError.

ATENÇÃO : Este é um hack, não um recurso. Não use isso em nenhum código (que não seja código-golf) que outro ser humano possa ver ou usar.


No meu caso eu recebo: TypeError: <lambda>() takes exactly 1 positional argument (2 given). Você tem certeza do IndexError?
Jovik

4
Sim. Você talvez forneceu o número errado de argumentos? Se você precisar de uma função lambda que possa receber qualquer número de argumentos, use lambda *x: [][0]. (A versão original só tem um argumento, pois nenhum argumento, use lambda : [][0]; para dois, uso lambda x,y: [][0]; etc.)
Kyle Strand

3
Eu expandi isso um pouco: lambda x: {}["I want to show this message. Called with: %s" % x] Produz: KeyError: 'I want to show this message. Called with: foo'
ErlVolton 14/10

@ErlVolton Clever! Embora usando isso em qualquer lugar exceto em um one-off roteiro parece ser uma idéia terrível ...
Kyle Strand

Estou temporariamente usando testes de unidade para um projeto em que não me incomodei em fazer uma zombaria real do meu criador de logs. Isso aumenta se você tentar registrar um erro ou crítica. Então ... Sim terrível, embora :) consensual
ErlVolton

10

Gostaria de dar uma explicação da ATUALIZAÇÃO 3 da resposta fornecida por Marcelo Cantos:

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

Explicação

lambda: 0é uma instância da builtins.functionclasse
type(lambda: 0)é a builtins.functionclasse
(lambda: 0).__code__é um codeobjeto.
Um codeobjeto é um objeto que contém o código de código compilado entre outras coisas. É definido aqui no CPython https://github.com/python/cpython/blob/master/Include/code.h . Seus métodos são implementados aqui https://github.com/python/cpython/blob/master/Objects/codeobject.c . Podemos executar a ajuda no 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__)é a classe de código.
Então, quando dizemos

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

estamos chamando o construtor do objeto de código com os seguintes argumentos:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • stacksize = 1
  • flags = 67
  • codestring = b '| \ 0 \ 202 \ 1 \ 0'
  • constantes = ()
  • names = ()
  • varnames = ('x',)
  • filename = ''
  • name = ''
  • firstlineno = 1
  • lnotab = b ''

Você pode ler sobre o significado dos argumentos na definição de PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . O valor de 67 para o flagsargumento é por exemploCO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE .

O argumento mais importante e o codestringque contém códigos de instrução. Vamos ver o que eles significam.

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

A documentação dos opcodes pode ser encontrada aqui https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . O primeiro byte é o código de operação LOAD_FAST, o segundo byte é o seu argumento, ou seja, 0.

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

Então, colocamos a referência xna pilha. A varnamesé uma lista de cadeias contendo apenas 'x'. Empurraremos o único argumento da função que estamos definindo para a pilha.

O próximo byte é o opcode para RAISE_VARARGSe o próximo byte é o seu argumento, ou seja, 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)

O TOS é o topo da pilha. Como colocamos o primeiro argumento ( x) da nossa função na pilha e argcé 1, aumentamos o xif se for uma instância de exceção ou criamos uma instância xe aumentamos o contrário.

O último byte, ou seja, 0, não é usado. Não é um código de operação válido. Pode muito bem não estar lá.

Voltando ao snippet de código, estamos a qualquer momento:

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

Chamamos o construtor do objeto de código:

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

Passamos o objeto de código e um dicionário vazio para o construtor de um objeto de função:

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

Vamos chamar ajuda em um objeto de função para ver o que os argumentos significam.

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

Em seguida, chamamos a função construída que passa uma instância de Exception como argumento. Conseqüentemente, chamamos uma função lambda que gera uma exceção. Vamos executar o trecho e ver se ele realmente funciona como pretendido.

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

Melhorias

Vimos que o último byte do bytecode é inútil. Não vamos desordenar essa expressão complicada com agulhas. Vamos remover esse byte. Além disso, se quisermos jogar um pouco de golfe, poderíamos omitir a instanciação de Exception e, em vez disso, passar a classe Exception como argumento. Essas alterações resultariam no seguinte código:

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

Quando executamos, obteremos o mesmo resultado de antes. É apenas mais curto.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.