Определите лямбда-выражение, которое вызывает исключение

137

Как я могу написать лямбда-выражение, которое эквивалентно:

def x():
    raise Exception()

Следующее не допускается:

y = lambda : raise Exception()
Томас Юнг
источник
2
Так что ты не можешь этого сделать. Используйте нормальные функции.
DrTyrsa
1
Какой смысл давать имя анонимной функции?
Джон Ла Рой
2
@gnibbler Вы можете использовать имя для ссылки на функцию. y () проще в использовании, чем (lambda: 0) () в REPL.
Томас Юнг
Так в чем же преимущество по y=lambda...сравнению с этим def y:?
Джон Ла Рой
@gnibbler Некоторый контекст: я хотел определить функцию def g (f, e), которая вызывает f в счастливом случае и e, если обнаружена ошибка. В зависимости от сценария e может вызвать исключение или вернуть какое-либо действительное значение. Чтобы использовать g, я хотел написать g (лямбда x: x * 2, лямбда e: рейз e) или g (лямбда x: x * 2, лямбда e: 0).
Томас Юнг

Ответы:

169

Существует несколько способов создания оболочки для Python:

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

Лямбды принимают заявления. Поскольку raise exэто утверждение, вы можете написать универсальный рейзер:

def raise_(ex):
    raise ex

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

Но если ваша цель состоит в том, чтобы избежать def, это, очевидно, не сокращает это. Это, однако, позволяет условно вызывать исключения, например:

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

В качестве альтернативы вы можете вызвать исключение без определения именованной функции. Все, что вам нужно, это сильный желудок (и 2.x для данного кода):

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

И сильный желудочный раствор python3 :

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

Спасибо @WarrenSpencer за указание очень простой ответ , если вы не заботитесь , которая возбуждается исключение: y = lambda: 1/0.

Марсело Кантос
источник
117
О боже, что это за темное искусство?
CodeColorist
11
Если вы не волнует , что тип исключения брошено также следующие работы: lambda: 1 / 0. Вы просто получите ZeroDivisionError вместо обычного исключения. Имейте в виду, что если исключение разрешено распространять, то для отладчика вашего кода может показаться странным, что он начинает видеть кучу ошибок ZeroDivisionErrors.
Уоррен Спенсер
Отличное решение @WarrenSpencer. Большая часть кода не имеет много ошибок с нулевым делением, поэтому она настолько уникальна, как если бы вы могли выбрать тип самостоятельно.
jwg
2
y = 1/0это супер-умное решение, если тип исключения не имеет значения
Сахер Ахвал
3
Кто-нибудь может рассказать нам о том, что на самом деле происходит в решениях «темное искусство / сильный желудок»?
декаль
56

Как насчет:

lambda x: exec('raise(Exception(x))')
vvkatwss vvkatwss
источник
12
Это довольно забавно, но для написания тестов, где вы хотите смоделировать функции, это работает аккуратно !!!
Каннан Эканат
8
Работает, но вы не должны этого делать.
Авгурар
1
Это не работает для меня, я получаю SyntaxErrorна Python 2.7.11.
Ник Свитинг
Я также получаю вышеупомянутую ошибку (SyntaxError) на Python 2.7.5
Динеш
1
это специфично для Python 3, однако я не думаю, что Python 2 позволяет это.
Сахер Ахвал
16

На самом деле, есть способ, но он очень надуманный.

Вы можете создать объект кода с помощью compile()встроенной функции. Это позволяет вам использовать raiseоператор (или любой другой оператор, в этом отношении), но это поднимает другую проблему: выполнение объекта кода. Обычный способ - использовать execоператор, но это возвращает вас к исходной проблеме, а именно к тому, что вы не можете выполнять операторы в lambda(или в eval()этом отношении).

Решение взломать. Вызываемые элементы, такие как результат lambdaоператора, имеют атрибут __code__, который может быть заменен. Таким образом, если вы создадите вызываемый объект и замените его __code__значение на объект кода из приведенного выше, вы получите что-то, что может быть оценено без использования операторов. Однако достижение всего этого приводит к очень непонятному коду:

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

Выше делает следующее:

  • compile()вызов создает объект кода , который вызывает исключение;

  • то lambda: 0возвращается отзывной , который не делает ничего , но возвращает значение 0 - используется для выполнения выше код объекта позже;

  • the lambda x, y, zсоздает функцию, которая вызывает __setattr__метод первого аргумента с оставшимися аргументами и возвращает первый аргумент! Это необходимо, потому что __setattr__само возвращается None;

  • map()вызов принимает результат lambda: 0, и использование lambda x, y, zего заменяет в __code__объект с результатом compile()вызова. Результатом этой операции сопоставления является список с одной записью, возвращаемой lambda x, y, z, поэтому нам нужно это lambda: если бы мы использовали __setattr__сразу, мы потеряли бы ссылку на lambda: 0объект!

  • наконец, выполняется первый (и единственный) элемент списка, возвращаемый map()вызовом, в результате чего вызывается объект кода, что в конечном итоге вызывает желаемое исключение.

Это работает (протестировано в Python 2.6), но это определенно не красиво.

Последнее замечание: если у вас есть доступ к typesмодулю (который потребует использования importоператора перед вашим eval), то вы можете немного сократить этот код: используя types.FunctionType()вы можете создать функцию, которая будет выполнять данный объект кода, так что вы выиграли не нужно создавать фиктивную функцию lambda: 0и заменять значение ее __code__атрибута.

Майкл Скарпа
источник
15

Функции, созданные с помощью лямбда-форм, не могут содержать операторов .

Mitar
источник
2
Это правда, но на самом деле это не отвечает на вопрос; Вызвать исключение из лямбда-выражения легко (а иногда его можно обнаружить с помощью очень надуманных приемов, см. stackoverflow.com/a/50916686/2560053 или stackoverflow.com/a/50961836/2560053 ).
Томас Барухель
15

Если все, что вам нужно, это лямбда-выражение, которое вызывает произвольное исключение, вы можете сделать это с помощью недопустимого выражения. Например, lambda x: [][0]попытается получить доступ к первому элементу в пустом списке, что вызовет IndexError.

ПОЖАЛУЙСТА, ОБРАТИТЕ ВНИМАНИЕ : это взломать, а не функция. Не используйте это в любом коде (не в коде), который другой человек может увидеть или использовать.

Кайл Стрэнд
источник
В моем случае я получаю: TypeError: <lambda>() takes exactly 1 positional argument (2 given). Вы уверены в IndexError?
Йовик
4
Ага. Возможно, вы указали неверное количество аргументов? Если вам нужна лямбда-функция, которая может принимать любое количество аргументов, используйте lambda *x: [][0]. (Оригинальная версия принимает только один аргумент; без аргументов - используйте lambda : [][0]; для двух - используйте lambda x,y: [][0]; и т. Д.)
Кайл Стрэнд,
3
Я немного расширил это: lambda x: {}["I want to show this message. Called with: %s" % x] Производит: KeyError: 'I want to show this message. Called with: foo'
ErlVolton
@ErlVolton Умный! Хотя использование этого в любом месте, кроме как в одноразовом сценарии, кажется ужасной идеей ...
Кайл Стрэнд,
Я временно использую в модульных тестах для проекта, где я не удосужился сделать настоящий макет моего регистратора. Это поднимается, если вы пытаетесь записать ошибку или критическое. Так что ... Да, ужасно, хотя и по обоюдному согласию :)
ErlVolton
10

Я хотел бы дать ОБНОВЛЕНИЕ 3 ответа, предоставленного Марсело Кантос:

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

объяснение

lambda: 0это экземпляр builtins.functionкласса.
type(lambda: 0)это builtins.functionкласс.
(lambda: 0).__code__это codeобъект. Объект является объектом , который содержит скомпилированный байткод среди других вещей. Это определено здесь в CPython https://github.com/python/cpython/blob/master/Include/code.h . Его методы реализованы здесь https://github.com/python/cpython/blob/master/Objects/codeobject.c . Мы можем запустить справку по объекту кода:
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__)это класс кода
Поэтому, когда мы говорим,

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

мы вызываем конструктор объекта кода со следующими аргументами:

  • argcount = 1
  • kwonlyargcount = 0
  • nlocals = 1
  • STACKSIZE = 1
  • Флаги = 67
  • codestring = Ь '| \ 0 \ 202 \ 1 \ 0'
  • Константы = ()
  • имена = ()
  • = имя переменный ( 'х',)
  • имя файла = «»
  • имя = «»
  • firstlineno = 1
  • lnotab = B ''

Вы можете прочитать о том, что значат аргументы, в определении PyCodeObject https://github.com/python/cpython/blob/master/Include/code.h . flagsНапример, значение 67 для аргумента CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE.

Наиболее важным аргументом является аргумент, codestringкоторый содержит коды операций. Посмотрим, что они значат.

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

Документацию по кодам операций можно найти здесь https://docs.python.org/3.8/library/dis.html#python-bytecode-instructions . Первый байт - это код операции LOAD_FAST, второй байт - его аргумент, т. Е. 0.

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

Поэтому мы помещаем ссылку xв стек. Это varnamesсписок строк, содержащих только 'x'. Мы поместим единственный аргумент функции, которую мы определяем, в стек.

Следующий байт - это код операции, RAISE_VARARGSа следующий байт - его аргумент, т. Е. 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 является вершиной стека. Так как мы поместили первый аргумент ( x) нашей функции в стек и равны argc1, мы будем увеличивать x если это экземпляр исключения, или делать его, xи поднимать его в противном случае.

Последний байт, т. Е. 0, не используется. Это не действительный код операции. С таким же успехом может и не быть.

Возвращаясь к фрагменту кода, мы в любом случае:

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

Мы вызвали конструктор объекта кода:

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

Мы передаем объект кода и пустой словарь в конструктор объекта функции:

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

Давайте вызовем справку для объекта функции, чтобы увидеть, что означают аргументы.

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

Затем мы вызываем созданную функцию, передавая экземпляр Exception в качестве аргумента. Следовательно, мы вызвали лямбда-функцию, которая вызывает исключение. Давайте запустим фрагмент и посмотрим, что он действительно работает как задумано.

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

улучшения

Мы видели, что последний байт байт-кода бесполезен. Давайте не будем загромождать это сложное выражение иголками. Давайте удалим этот байт. Также, если мы хотим немного поиграть в гольф, мы можем опустить создание Exception и вместо этого передать класс Exception в качестве аргумента. Эти изменения приведут к следующему коду:

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

Когда мы запустим его, мы получим тот же результат, что и раньше. Это просто короче.

кацу
источник