Допустим, у меня есть следующий код:
def returns_false():
breakpoint()
return False
assert(returns_false())
print("Hello world")
Существует ли последовательность команд pdb, которая будет печатать «Hello world» без предварительного запуска AssertionError?
Я не могу изменить ни одного символа этого исходного файла, я только ищу, чего я могу достичь, пока он уже запущен.
Что я пробовал:
return True
s
, чтобы вернуться в режим возврата, а затем либо retval=True
, либо locals()['__return__']=True
interact
, а затем return True
, но это вызывает исключениеНо ничто из этого не меняет фактическое возвращаемое значение.
Да, но когда вы находитесь в функции, вне функции нет строки, к которой можно было бы перейти, и при выходе из функции, кажется, что утверждение оценивается до того, как я смогу перейти. Так что в основном s s s j 6 не работает :-(
Идея состоит в том, чтобы перепрыгнуть через функцию, вам не нужно заходить внутрь нее.
Однако точка останова() находится внутри функции, поэтому именно здесь она и прервется.
Что ж, тогда вы пытались опустить такое утверждение, например: python -Om pdb <Your_Module_Name>.py
Я прочитал это. Я довольно много играл с sys.settrace(), и мой опыт показывает, что если переназначить значение args при событиях возврата, это действительно повлияет на новое значение переменной, но это значение не отразится на том, что фактически вернулся к следующему вызову. Конечно, возможно, я неправильно понял, поэтому задаю вопросы по SO ^^, но я думаю, что на данный момент вы можете поверить, что я серьезно изучил этот вопрос и проявил должную осмотрительность в меру своих возможностей. возможности (которые они собой представляют :shrugh: )
Чего я не понимаю, так это того, что ты выглядишь особенно недовольным. Я принял ответ на этот вопрос, это для тебя проблема?
Для решения вашей проблемы существует как минимум 6 различных методов, каждый из которых имеет свои плюсы и минусы. Однако все эти методы требуют некоторых изменений в коде и не используют pdb. Существует метод с использованием pdb, который позволяет минимизировать изменения в коде. Поскольку метод pdb можно применять только в определенных ситуациях и он менее питоничен, я поставлю его последним.
Самое очевидное и простое решение — изменить порядок расположения assert
и print
, например:
def returns_false():
breakpoint()
return False
print("Hello world")
assert(returns_false())
Еще один простой способ:
def returns_false():
breakpoint()
return True
assert(returns_false())
print("Hello world")
Однако, если вы действительно не хотите менять код, рассмотрите возможность запуска файла Python в командной строке следующим образом:
python -O test.py
Однако это отключит все assert
в файле Python. Если вы все еще этого не хотите, вы можете попробовать еще три варианта:
try
/except
, чтобы поймать AssertionError
, например:def returns_false():
breakpoint()
return False
try:
assert(returns_false())
except AssertionError:
print('Assertion Error caught')
print("Hello world")
Это будет подавлять только это assert
, а не других.
try
/except
:class AssertSuppressor():
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return True
with AssertSuppressor():
assert (returns_false())
print("Hello world")
Это подавит assert
так же, как try
/except
. Проблема этого подхода в том, что print
должен располагаться за пределами блока with
. Более причудливый подход решит эту проблему:
class AssertSuppressorFancy():
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.func(*self.args, **self.kwargs)
# return True here if you still want to suppress the assert, but I don't think it is necessary
with AssertSuppressorFancy(print, 'Hello World'):
assert(returns_false())
Приведенный выше контекстный менеджер принимает вызываемый аргумент func
и аргументы (как ключевые слова, так и позиционные) для передачи вызываемому объекту. Он вызывает func
с *args
и **kwargs
, когда блок with
завершен, независимо от того, есть исключение или нет.
def wrapper(func):
def inner(*args, **kwargs):
ret = func(*args, **kwargs)
if ret:
return ret
return True
return inner
@wrapper
def returns_false():
breakpoint()
return False
assert(returns_false())
print("Hello world")
Здесь я использовал вложенную функцию, чтобы можно было использовать функцию wrapper
в качестве декоратора. Конечно, wrapper
можно использовать напрямую, не применяя его в качестве декоратора. Эта функция изменяет возвращаемое значение return_false
на True. Он будет работать с любой функцией, поскольку проверяет, возвращает ли она истинное значение; если да, то это не беспокоит; если нет, возвращается True
.
Запустить print('Hello World')
без запуска AssertionError
лучше всего с помощью методов, указанных выше; то есть либо а). порядок assert
и print
поменять местами (код 1); б). изменить возвращаемое значение функции (код 2); или в). одна из перечисленных выше реализаций (try
/except
, менеджер контекста, декоратор/обертка). Конечно, установка флага -O
— еще один жизнеспособный вариант, но если вы хотите, чтобы другие assert
в вашем коде работали, то это может быть не лучшим выбором.
Наконец, хотя я настоятельно рекомендую один из вышеперечисленных методов, на самом деле есть способ пропустить assert
с помощью pdb. Теперь, если по какой-то причине вам действительно нужно использовать только команды pdb, попробуйте:
import pdb
def returns_false():
# breakpoint() not needed here
return False
pdb.set_trace()
assert(returns_false())
print("Hello world")
Затем в консоли сделайте следующее:
> /home/repl/673e9267-383f-4c6f-9338-ccf0a50572a3/main.py(8)<module>()
-> assert(returns_false())
(Pdb)
l
3 def returns_false():
4 # breakpoint() not needed here
5 return False
6
7 pdb.set_trace()
8 -> assert(returns_false())
9 print("Hello world")
[EOF]
(Pdb)
j 9
> /home/repl/673e9267-383f-4c6f-9338-ccf0a50572a3/main.py(9)<module>()
-> print("Hello world")
(Pdb)
c
Hello world
Альтернативно, вы не хотите import pdb
и pdb.set_trace()
, есть аналогичный второй метод достижения цели с использованием breakpoint()
:
def returns_false():
# breakpoint() not needed here
return False
breakpoint()
assert(returns_false())
print("Hello world")
а затем это в консоли:
> /home/repl/f0e3ca08-1383-492f-9245-a1cc19b7f28d/main.py(6)<module>()
-> assert(returns_false())
(Pdb)
l
1 def returns_false():
2 # breakpoint() not needed here
3 return False
4
5 breakpoint()
6 -> assert(returns_false())
7 print("Hello world")
[EOF]
(Pdb)
j 7
> /home/repl/f0e3ca08-1383-492f-9245-a1cc19b7f28d/main.py(7)<module>()
-> print("Hello world")
(Pdb)
c
Hello world
Первоначально я думал, что использование только pdb не сработает, потому что, когда я попробовал описанный выше метод (и pdb.set_trace()
, и breakpoint()
), все, что я получил, это:
*** Jump failed: can only jump from a 'line' trace event
Он был напечатан после того, как я ввел команду j
(аббревиатура jump
). Однако позже я узнал, что между pdb, который здесь используется, и ipdb, потомком ipdb, который был установлен на моем компьютере, была какая-то проблема. Проще говоря, эти двое просто не ладят. Итак, будьте осторожны: если на вашем компьютере установлен ipdb, приведенный выше код НЕ будет работать и приведет к появлению загадочного сообщения об ошибке. Но если у вас нет ipdb, то приведенный выше код будет работать нормально. (Примечание. Я запустил приведенный выше код в онлайн-интерпретаторе Python, потому что не могу заставить его работать на своем компьютере. Я все еще пытаюсь найти проблему с ipdb, но пока не могу найти причину.)
Вы правы, если бы я мог использовать точку останова() вне функции, возвращающей false, я бы смог пропустить утверждение, но, к сожалению, в моем случае я тоже не могу, поэтому мне приходится использовать то, что мне дано. ! То есть я не могу изменить ни одной строки из предоставленного мной примера входных данных.
@C4stor А как насчет других методов? Или вы действительно не можете изменить ни одну строчку кода? Если да, то извините, но я думаю, что перепрыгнуть assert
невозможно. Вероятно, вам следует попытаться опустить все assert
, используя метод python -O test.py
, указанный в моем посте.
@C4stor В своем описании вы упомянули, что хотели бы получить подробное описание структуры выполнения Python. Я думаю, что могу добавить это как еще одно решение (потому что мое исходное решение предполагает, что вы можете изменить хотя бы немного кода. Кроме того, оно уже слишком длинное). Хотите, чтобы я улучшил существующий ответ или добавил новый?
@ C4stor Я исследовал всевозможные методы, но ни один из них, похоже, не работает. Не могли бы вы объяснить вашу проблему немного подробнее? Например, является ли ваш пример реальным кодом, с которым вы имеете дело? И правда ли, что вы действительно не можете изменить ни одной строчки кода? Если да, то объясните, почему. Я нашел массу методов, которые работали бы, если бы можно было изменить хотя бы несколько строк кода. Кроме того, почему вы можете использовать breakpoint()
и pdb
, если вы не можете изменить какой-либо код? Оно уже есть? Добавление этих деталей действительно ОЧЕНЬ поможет.
Привет :-) Я максимально упростил задачу, чтобы ее можно было воспроизвести. На практике происходит следующее: - я могу запустить python -m pdb с нуля - я начинаю отладку - я ввожу функцию f() в используемую нами библиотеку (поэтому я не могу прикасаться к коду) - метод завершается on return(lib_call()) , и мне нужно смоделировать, что происходит с различными возвращаемыми значениями в lib_call() - поэтому на этом этапе я бы хотел заставить f() возвращать любое значение, которое я хочу, и двигаться вперед. Вопрос предназначен для правильной эмуляции этих условий без огромной настройки.
Я также много чего пробовал, сейчас проверяю, могу ли я изменить байт-код в Inspect.currentframe().f_code.co_code , но, похоже, это не работает. Я также попробовал использовать новый sys.monitoring с обратным вызовом на Py_RETURN, обратный вызов вызывается, но изменение retval ничего не дает :-( (также я подозреваю, что если retval является ссылкой, а не просто «False», возможно, это сработает)
@C4stor Однако новое решение по-прежнему добавляет строку ret_val=True
в код функции. Это правда, что если ret_val
является ссылкой на переменную, то методы pdb будут работать, и мое новое решение основано именно на этом. Я попробовал несколько раз, и получилось хорошо. В настоящее время я изучаю другие методы, например, изменение байт-кодов.
Если вы не хотите менять положение breakpoint()
, проверьте это:
def return_false():
breakpoint()
ret_val = False # must define ret_val here
return ret_val
assert(return_false())
print('Hello World')
Затем в консоли:
> /home/repl/aa5ee727-4230-4aa9-b79d-9b3ca437e234/main.py(3)return_false()
-> ret_val = False
(Pdb)
l
1 def return_false():
2 breakpoint()
3 -> ret_val = False
4 return ret_val
5
6
7 assert(return_false())
8 print('Hello World')
[EOF]
(Pdb)
j 4
> /home/repl/aa5ee727-4230-4aa9-b79d-9b3ca437e234/main.py(4)return_false()
-> return ret_val
(Pdb)
ret_val=True
(Pdb)
c
Hello World
Однако приведенный выше метод по-прежнему меняет код внутри функции. Если добавление строки ret_val=True
неприемлемо, то это вам не подойдет. Однако существует высокая вероятность того, что подобная строка в коде уже будет.
В любом случае, попробуйте приведенный выше код.
Опять же, приведенный выше код выполняется в онлайн-интерпретаторе Python, поскольку он не работает на моем компьютере.
Это действительно возможно в динамическом языке, таком как Python, где код интерпретируется построчно и может быть самоизменяющимся.
Вам придется много печатать, но вот что вы можете ввести в отладчик:
import ctypes
tup = returns_false.__code__.co_consts
obj = ctypes.py_object(tup)
pos = ctypes.c_ssize_t(1)
o = ctypes.py_object(True)
ref_count = ctypes.c_long.from_address(id(tup))
original_count = ref_count.value
ref_count.value = 1
ctypes.pythonapi.Py_IncRef(o)
ctypes.pythonapi.PyTuple_SetItem(obj, pos, o)
ref_count.value = original_count
c
Это изменяет само возвращаемое значение в памяти и приведет к тому, что returns_false
вернет True
вместо False
. Последующее утверждение после выхода из pdb пройдет. Здесь нет никакой ловкости рук и никаких изменений исходных исходных файлов, мы буквально меняем возвращаемое значение во время выполнения.
Последняя строка здесь «c» — это ярлык для «продолжить» в pdb и завершает отладчик.
Здесь я использую Python 3.12.4. Некоторые детали реализации могут отличаться в других версиях Python, но должна работать та же основная техника.
Для исходного исходного кода:
def returns_false():
breakpoint()
return False
assert(returns_false())
print("Hello world")
Рассмотрим дизассемблирование вашей функции с использованием модуля stdlib dis:
>>> import dis
>>> dis.dis(returns_false)
1 0 RESUME 0
2 2 LOAD_GLOBAL 1 (NULL + breakpoint)
12 CALL 0
20 POP_TOP
3 22 RETURN_CONST 1 (False)
Последняя строка указывает возвращаемое значение RETURN_CONST (False). В последней строке дизассемблирования вы также увидите три цифры:
3
относится к номеру строки оператора возврата в исходном файле.22
— это смещение этой инструкции в байт-коде.1
— это аргумент операции RETURN_CONST.Последний пункт интересен. На самом деле это означает, что функция возвращает элемент 1 из таблицы констант объекта функции. dis
услужливо указал в скобках, что этот элемент имеет значение «False», но он просто отображает любой элемент 1 в таблице констант:
>>> returns_false.__code__.co_consts
(None, False)
Таблица констант будет длиннее, если в теле функции будет больше констант, например, если вы добавили строку x = 1234
внутри функции, которую ожидали увидеть 1234
в таблице констант, и возвращаемое значение теперь будет находиться по индексу 2. вместо этого (pos
в моем примере также придется соответствующим образом изменить).
Итак, таблица констант представляет собой кортеж, который является неизменяемым типом, но что, если бы мы могли изменить этот кортеж (в Python все изменяемо, если вы знаете, где искать). Изменит ли изменение элемента с индексом 1 возвращаемое значение функции? Действительно, так и было бы.
Частью C API является PyTuple_SetItem
int PyTuple_SetItem(PyObject *p, Py_ssize_t pos, PyObject *o)
Вставьте ссылку на объект o в позицию pos кортежа, на который указывает p. Верните
0
в случае успеха. Если pos выходит за пределы, верните-1
и установите исключениеIndexError
.
И разработчики CPython даже настолько полезны, что предоставляют Python API в ctypes.pythonapi
для использования таких функций, как PyTuple_SetItem
, непосредственно из среды выполнения.
Остальная часть ответа — это вопрос техники и некоторых ноу-хау в реализации, стараясь не вызвать сегфолт или не испортить подсчет ссылок.
Обратите внимание, что дизассемблирование «взломанной» функции будет учитывать обновления таблицы констант и теперь будет отображать RETURN_CONST с (True):
>>> returns_false.__code__.co_consts
(None, False)
>>> import ctypes
... tup = returns_false.__code__.co_consts
... obj = ctypes.py_object(tup)
... pos = ctypes.c_ssize_t(1)
... o = ctypes.py_object(True)
... ref_count = ctypes.c_long.from_address(id(tup))
... original_count = ref_count.value
... ref_count.value = 1
... ctypes.pythonapi.Py_IncRef(o)
... ctypes.pythonapi.PyTuple_SetItem(obj, pos, o)
... ref_count.value = original_count
...
>>> returns_false.__code__.co_consts
(None, True)
>>> returns_false()
> /private/tmp/p.py(3)returns_false()
-> return False
(Pdb) c
True
>>> import dis
>>> dis.dis(returns_false)
1 0 RESUME 0
2 2 LOAD_GLOBAL 1 (NULL + breakpoint)
12 CALL 0
20 POP_TOP
3 22 RETURN_CONST 1 (True)
Наконец, я упомяну, что RETURN_CONST — не единственный код операции возврата для функции, фактически он является новым в Python 3.12. RETURN_VALUE, вероятно, встречается чаще. В других Python 3.x (я проверял 3.6-3.11) ваш код будет использовать RETURN_VALUE
op, но тот же патч, дословно введенный в отладчике, будет работать, потому что он просто будет возвращать значение, которое было ранее загружено из констант. таблицу в стек. Детали дизассемблирования будут выглядеть иначе в Python 3.6-3.10, 3.11 и 3.12.
Вас также могут заинтересовать вопросы и ответы можно ли манипулировать локальную переменную, введенную в тело функции? где я продемонстрировал аналогичный метод изменения локальных переменных функции. В этом ответе я также изменил возврат функции, которая использовала код операции RETURN_VALUE
.
Не тот, кто голосовал против, но я полагаю, что отрицательный голос возник из-за многословия и хрупкости хака ctypes
, но я считаю, что это хорошее чтение. Проголосовал за восстановление и +1 для вас.
Обратите внимание, что изменение co_consts
работает только в том случае, если функция вообще возвращает константу, и этот подход не будет работать, если функция возвращает результат выражения, например, с return x == y
.
@blhsing Я могу ответить на вопрос только так, как он задан. Подобный метод всегда возможен в ctypes или даже непосредственно в Python — в этом случае вы можете использовать MonkeyPatch для type(x).__eq__
или просто заменить значение x
непосредственно в отладчике.
Это чрезвычайно хрупко и на самом деле не является способом изменить возвращаемые значения. Он изменяет кортеж констант функции, что влияет на все случаи использования этой константы в любом месте кода функции. Если функция использует константу не только для возвращаемого значения, это повлияет не только на то, о чем просит вопрос, а если функция не возвращает константу (например, return sum(l)
), она вообще не будет работать. . Изменение кортежа констант оказывается достаточным для этой очень специфической функции, но оно не обобщает. Кроме того, это влияет на все будущие вызовы, а не только на текущий вызов.
ОП упоминает, что он/она ищет «общеприменимое решение», поэтому я думаю, что ошибка все еще заметна.
@user2357112 user2357112 Если вы не хотите, чтобы это повлияло на другое использование таблицы consts, вы переходите к оператору return перед применением исправления, а затем сразу же после этого отменяете исправление.
Нельзя отрицать, что он действительно отвечает на заданный вопрос - я действительно буду искать более общее решение для непостоянной доходности, но я думаю, что это заслуживает того, чтобы его принять.
Это специальный хак, который не отвечает на вопрос, можно ли стандартными средствами выйти из исполняемой функции в произвольном месте, вернув произвольное значение.
Возврат константы — самый трудный случай для Monkeypatch. Переопределить возвращаемые значения переменных в сравнении легко — вы можете просто заменить переменную в отладчике. Если код вызывающего абонента можно изменить, проблема тривиальна (например, ock.patch ). В каком-то смысле C4stor сформулировал вопрос самым сложным образом.
@Vitalizzare Я согласен, но вопрос сформулирован не так. Мне все равно было бы очень любопытно получить ответ на более общий вопрос, поэтому я подготовлю новый вопрос с лучшей формулировкой. Я думаю, что этот вопрос сам по себе заслуживает внимания, и хотя ответ на него ситуативный, из него еще есть чему поучиться (по крайней мере, я научился). У меня достаточно репутации, чтобы выставить больше наград в погоне за Граалем :-D
@wim Что бы вы сделали, если бы все вычисления были помещены после return
, вроде def f(x): return [x for x in range(x)]
? Не проще ли просто запустить модуль с помощью pdb
до интересующей линии, пропустить ее, прыгнув, и продолжить?
@Vitalizzare Хороший вопрос! Установите другую награду и я буду охотиться за ней ;)
@wim Этот вопрос был всего лишь контрпримером к утверждению, что замена константы — самое сложное. Если вы считаете, что вопрос заслуживает полного ответа, вы можете спросить его сами.
@Vitalizzare Вы правы, это случай, который я не учел, делая заявление. Даже def f(): return []
кажется, что обезьянку патчить будет сложно.
Вы можете установить точку останова на строке, которая вызывает returns_false
, когда вы загружаете скрипт с помощью pdb
, чтобы вы могли ввести перерыв перед вызовом, после чего вы можете исправить функцию, чтобы она возвращалась True
.
Пример сеанса с вашим примером сценария в качестве входных данных:
# python -mpdb test.py
> /root/test.py(1)<module>()
-> def returns_false():
(Pdb) b 5
Breakpoint 1 at /root/test.py:5
(Pdb) r
> /root/test.py(5)<module>()
-> assert(returns_false())
(Pdb) !returns_false = lambda: True
(Pdb) r
Hello world
--Return--
> /root/test.py(6)<module>()->None
-> print("Hello world")
(Pdb)
В случае, если returns_false
вызывается последующим кодом, вы можете сохранить исходную функцию и восстановить returns_false
с оригиналом после вызова функции-заглушки:
# python -mpdb test.py
> /root/test.py(1)<module>()
-> def returns_false():
(Pdb) b 5
Breakpoint 1 at /root/test.py:5
(Pdb) b 6
Breakpoint 2 at /root/test.py:6
(Pdb) r
> /root/test.py(5)<module>()
-> assert(returns_false())
(Pdb) !orig_returns_false = returns_false; returns_false = lambda: True
(Pdb) r
> /root/test.py(6)<module>()
-> print("Hello world")
(Pdb) !returns_false = orig_returns_false
(Pdb) r
Hello world
--Return--
> /root/test.py(6)<module>()->None
-> print("Hello world")
(Pdb)
Сначала вам нужно запустить скрипт:
python -m pdb your_script.py
Когда скрипт достигнет точки останова, введите следующее:
(Pdb) import code
(Pdb) co = returns_false.__code__
(Pdb) new_co = code.CodeType(co.co_argcount, co.co_posonlyargcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code[:-2] + b'\x01', co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars)
(Pdb) returns_false.__code__ = new_co
(Pdb) c
На третьем этапе создается новый объект кода с теми же свойствами, что и исходный, за исключением последних двух байтов атрибута co_code, который представляет собой байт-код для возврата False. Вам необходимо заменить последний байт на b'\x01', который является байт-кодом для возврата True.
Мне нравится эта идея, но, похоже, она не работает: 1. CodeType не является частью модуля кода, а является частью модуля типов, и это достаточно легко исправить. 2. Я попробовал это, и это не сработало, потому что две причины: а. Пока метод return_false() работает, он кажется безразличным к изменению b. Если я «обмажу» и отредактирую код метода перед его запуском, он больше не сможет работать (я полагаю, что конструктор неправильный, по крайней мере, не в Python 3.9 и 3.12).
Эта идея не работает, существующий код в памяти будет продолжать использоваться — ничего не будет следить за тем, чтобы вы динамически переназначили __code__
.
Вы пробовали это? stackoverflow.com/questions/27741387/…